storysplat-viewer 2.2.4 → 2.2.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/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/storysplat-viewer.umd.js +1 -1
- package/dist/storysplat-viewer.umd.js.map +1 -1
- package/dist/types/dynamic-viewer/CameraControls.d.ts +5 -0
- package/dist/types/dynamic-viewer/CharacterController.d.ts +5 -0
- package/dist/types/transformers/sceneToConfig.d.ts +5 -0
- package/dist/types/types/index.d.ts +3 -1
- package/package.json +2 -2
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":["../src/html-generation/generateHTML.ts","../src/transformers/sceneToConfig.ts","../src/dynamic-viewer/viewerUI.ts","../src/dynamic-viewer/CameraControls.ts","../src/dynamic-viewer/CharacterController.ts","../src/effects/gsplat-reveal-radial.ts","../src/effects/reveal-presets.ts","../node_modules/js-binary-schema-parser/lib/index.js","../node_modules/js-binary-schema-parser/lib/parsers/uint8.js","../node_modules/gifuct-js/lib/index.js","../node_modules/js-binary-schema-parser/lib/schemas/gif.js","../node_modules/gifuct-js/lib/deinterlace.js","../node_modules/gifuct-js/lib/lzw.js","../src/dynamic-viewer/AnimatedGifHelper.ts","../src/dynamic-viewer/HtmlMeshHelper.ts","../src/dynamic-viewer/CustomScriptSystem.ts","../src/dynamic-viewer/createViewer.ts","../src/dynamic-viewer/createViewerFromSceneId.ts"],"sourcesContent":["/**\n * Simple HTML Generator for StorySplat\n *\n * Generates standalone HTML that loads the viewer from CDN.\n * This replaces the old 13K+ line HTML generation system with a simple\n * bootstrap HTML that uses the same viewer code as dynamic embedding.\n */\n\nimport type { SceneData } from '../types';\n\nexport interface GenerateHTMLOptions {\n /** Custom CDN URL for the viewer bundle. Default: unpkg */\n cdnUrl?: string;\n /** HTML page title. Default: scene name */\n title?: string;\n /** Meta description for SEO */\n description?: string;\n /** Favicon URL */\n faviconUrl?: string;\n /** Custom CSS to inject */\n customCSS?: string;\n /** Whether to minify the output */\n minify?: boolean;\n}\n\n/**\n * Escape HTML entities in a string\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Escape string for safe embedding in inline <script> tags.\n * Prevents XSS by escaping </script> sequences that would close the script tag prematurely.\n */\nfunction escapeForInlineScript(str: string): string {\n // Escape </script> (case-insensitive) to prevent script tag injection\n // Using Unicode escapes for the < character within script context\n return str.replace(/<\\/script/gi, '<\\\\/script');\n}\n\n/**\n * Generate standalone HTML for a StorySplat scene\n *\n * @param sceneData - Scene data object (same format as createViewer)\n * @param options - Generation options\n * @returns Complete HTML string ready to be saved as .html file\n *\n * @example\n * ```typescript\n * import { generateHTML } from 'storysplat-viewer';\n *\n * const html = generateHTML(sceneData, { title: 'My Scene' });\n * // Save html to file or serve it\n * ```\n */\nexport function generateHTML(sceneData: SceneData, options: GenerateHTMLOptions = {}): string {\n const {\n cdnUrl = 'https://unpkg.com/storysplat-viewer@2/dist/storysplat-viewer.umd.js',\n title = sceneData.name || 'StorySplat Scene',\n description = `Interactive 3D scene: ${sceneData.name || 'StorySplat'}`,\n faviconUrl,\n customCSS = ''\n } = options;\n\n const faviconTag = faviconUrl\n ? `<link rel=\"icon\" href=\"${escapeHtml(faviconUrl)}\" />`\n : '';\n\n const customCSSBlock = customCSS\n ? `<style>${customCSS}</style>`\n : '';\n\n // Serialize scene data, handling circular references\n // Then escape for safe embedding in inline <script> tags\n const rawJSON = JSON.stringify(sceneData, (key, value) => {\n // Skip any DOM nodes or circular refs\n // Check for HTMLElement safely (it doesn't exist in Node.js)\n if (typeof HTMLElement !== 'undefined' && value instanceof HTMLElement) return undefined;\n if (typeof value === 'function') return undefined;\n // Skip any object with nodeType (DOM nodes)\n if (value && typeof value === 'object' && 'nodeType' in value) return undefined;\n return value;\n }, 2);\n // Escape </script> sequences to prevent XSS attacks\n const sceneDataJSON = escapeForInlineScript(rawJSON);\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n <meta name=\"description\" content=\"${escapeHtml(description)}\">\n <title>${escapeHtml(title)}</title>\n ${faviconTag}\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body {\n width: 100%;\n /* Use 100dvh for iOS address bar support, with 100vh fallback */\n height: 100vh;\n height: 100dvh;\n overflow: hidden;\n background: #111;\n }\n #app {\n width: 100%;\n /* Use 100dvh for iOS address bar support, with 100vh fallback */\n height: 100vh;\n height: 100dvh;\n position: relative;\n }\n /* Loading state */\n #app:empty::after {\n content: 'Loading...';\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n color: #666;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 16px;\n }\n </style>\n ${customCSSBlock}\n</head>\n<body>\n <div id=\"app\"></div>\n\n <script src=\"${escapeHtml(cdnUrl)}\"></script>\n <script>\n (function() {\n 'use strict';\n\n var sceneData = ${sceneDataJSON};\n\n // Wait for DOM and viewer to be ready\n function init() {\n var container = document.getElementById('app');\n if (!container) {\n console.error('[StorySplat] Container #app not found');\n return;\n }\n\n if (typeof StorySplatViewer === 'undefined' || !StorySplatViewer.createViewer) {\n console.error('[StorySplat] Viewer not loaded. Check CDN URL.');\n return;\n }\n\n try {\n var viewer = StorySplatViewer.createViewer(container, sceneData, {\n showUI: true,\n autoPlay: false\n });\n\n viewer.on('ready', function() {\n console.log('[StorySplat] Scene ready');\n });\n\n viewer.on('error', function(err) {\n console.error('[StorySplat] Error:', err);\n });\n\n // Expose viewer globally for debugging\n window.storySplatViewer = viewer;\n } catch (err) {\n console.error('[StorySplat] Failed to create viewer:', err);\n }\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n } else {\n init();\n }\n })();\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Generate HTML from a scene URL (fetches the JSON first)\n *\n * @param jsonUrl - URL to fetch scene JSON from\n * @param options - Generation options\n * @returns Complete HTML string\n */\nexport async function generateHTMLFromUrl(\n jsonUrl: string,\n options: GenerateHTMLOptions = {}\n): Promise<string> {\n const response = await fetch(jsonUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch scene: ${response.statusText}`);\n }\n const sceneData: SceneData = await response.json();\n return generateHTML(sceneData, options);\n}\n","/**\n * Scene Data to ExportProps Transformer\n *\n * Converts Firebase scene JSON or direct scene data to the ExportProps format\n * used by the HTML generation system.\n */\n\nimport type { SceneData, WaypointData, ExportProps, SplatSwapPoint } from '../types';\n\n/**\n * Transform scene data to ExportProps format\n */\nexport function transformSceneToExportProps(scene: SceneData): ExportProps {\n // Get all available URLs\n const originalUrl = scene.loadedModelUrl || scene.splatUrl || '';\n const sogUrl = scene.sogModelUrl || scene.sogUrl;\n const compressedPlyUrl = scene.compressedPlyUrl;\n const lodMetaUrl = scene.lodMetaUrl; // LOD streaming format (highest priority)\n\n // For PlayCanvas, prefer formats in this order: LOD > SOG > Compressed PLY > PLY > Original\n // PlayCanvas can't directly load .splat files, so we need PLY-compatible formats\n const originalExt = originalUrl.split('?')[0].split('.').pop()?.toLowerCase();\n const isPlayCanvasCompatible = originalExt === 'ply' || originalUrl.includes('.compressed.ply');\n\n // Choose the best URL for PlayCanvas (for backward compatibility, splatUrl is the main URL)\n // LOD streaming is handled separately as lodMetaUrl\n let splatUrl = originalUrl;\n if (sogUrl) {\n splatUrl = sogUrl; // SOG is best (after LOD)\n } else if (compressedPlyUrl) {\n splatUrl = compressedPlyUrl; // Compressed PLY is second best\n } else if (!isPlayCanvasCompatible) {\n // Original is not compatible, keep it but warn\n console.warn('[StorySplat Viewer] Original file format may not be compatible with PlayCanvas:', originalExt);\n }\n\n console.log('[StorySplat Viewer] URL selection:', {\n original: originalUrl,\n lodMetaUrl,\n sogUrl,\n compressedPlyUrl,\n selected: splatUrl\n });\n\n // Transform waypoints\n const waypoints = (scene.waypoints || []).map((wp: WaypointData) => {\n // Handle FOV - convert from radians to degrees if necessary\n // (matching waypointNavigation.ts logic)\n let fov = wp.fov || 60;\n if (fov < 3.5) {\n // FOV is in radians, convert to degrees\n fov = fov * (180 / Math.PI);\n }\n\n // Extract info from interactions (type: 'info') if not directly on waypoint\n // In SceneTypes.ts, waypoint info is stored in interactions array with type 'info'\n let info = (wp as any).info || (wp as any).description || '';\n if (!info && wp.interactions) {\n const infoInteraction = wp.interactions.find((i: any) => i.type === 'info');\n if (infoInteraction && infoInteraction.data && (infoInteraction.data as any).text) {\n info = (infoInteraction.data as any).text;\n }\n }\n\n return {\n position: wp.position || { x: wp.x || 0, y: wp.y || 0, z: wp.z || 0 },\n rotation: wp.rotation || { x: 0, y: 0, z: 0 },\n fov,\n duration: wp.duration || 2000,\n name: wp.name || (wp as any).title || '',\n info,\n interactions: wp.interactions || [],\n triggerDistance: (wp as any).triggerDistance ?? 1.0\n };\n });\n\n // Build ExportProps\n const exportProps: ExportProps = {\n // Scene Metadata\n name: scene.name || 'StorySplat Scene',\n sceneId: scene.sceneId,\n userId: scene.userId,\n userName: scene.userName || 'Unknown', // Fallback for anonymous/deleted users\n thumbnailUrl: scene.thumbnailUrl,\n\n // Splat Configuration\n splatUrl,\n sogUrl,\n lodMetaUrl, // LOD streaming format URL (highest priority)\n // Build fallback URLs from all available formats (excluding the primary)\n fallbackUrls: [\n ...(scene.fallbackUrls || []),\n // Add other available formats as fallbacks\n sogUrl !== splatUrl ? sogUrl : null,\n compressedPlyUrl !== splatUrl ? compressedPlyUrl : null,\n originalUrl !== splatUrl ? originalUrl : null\n ].filter((url): url is string => url != null && url !== ''),\n // Handle both splatScale (number) and scale (object) formats\n scale: scene.scale || (\n scene.splatScale != null\n ? { x: scene.splatScale, y: scene.splatScale, z: scene.splatScale }\n : { x: 1, y: 1, z: 1 }\n ),\n // Handle both array and object position formats\n splatPosition: scene.splatPosition || scene.position || scene.cameraPosition || [0, 0, 0],\n splatRotation: scene.splatRotation || scene.rotation || scene.cameraRotation || [0, 0, 0],\n invertXScale: scene.invertXScale,\n invertYScale: scene.invertYScale,\n\n // Scene Data\n waypoints,\n hotspots: scene.hotspots || [],\n portals: scene.portals || [],\n skybox: scene.skybox,\n customMeshes: scene.customMeshes || [],\n htmlMeshes: scene.htmlMeshes || [],\n lights: scene.lights || [],\n particles: scene.particles || [],\n collisionMeshesData: scene.collisionMeshesData || [],\n playerHeight: scene.playerHeight,\n\n // UI Configuration\n uiColor: scene.uiColor || '#ffffff',\n uiOptions: {\n showStartExperience: scene.uiOptions?.showStartExperience ?? true,\n showWatermark: scene.uiOptions?.showWatermark ?? true,\n hideFullscreenButton: scene.uiOptions?.hideFullscreenButton ?? false,\n hideInfoButton: scene.uiOptions?.hideInfoButton ?? false,\n hideMuteButton: scene.uiOptions?.hideMuteButton ?? false,\n hideHelpButton: scene.uiOptions?.hideHelpButton ?? false,\n hideWatermark: scene.uiOptions?.hideWatermark ?? false,\n watermarkText: scene.uiOptions?.watermarkText,\n watermarkLink: scene.uiOptions?.watermarkLink,\n buttonPosition: scene.uiOptions?.buttonPosition || 'inline',\n buttonLabels: scene.uiOptions?.buttonLabels,\n // Whitelabeling option for custom preloader logo\n customPreloaderLogoUrl: scene.uiOptions?.customPreloaderLogoUrl\n },\n\n // Camera Configuration\n defaultCameraMode: scene.defaultCameraMode || 'orbit',\n allowedCameraModes: scene.allowedCameraModes || ['orbit', 'first-person', 'drone'],\n cameraMovementSpeed: scene.cameraMovementSpeed,\n cameraRotationSensitivity: scene.cameraRotationSensitivity,\n\n // Navigation Configuration\n includeScrollControls: scene.includeScrollControls ?? true,\n scrollButtonMode: scene.scrollButtonMode || 'continuous',\n scrollAmount: scene.scrollAmount || 100,\n scrollSpeed: scene.scrollSpeed,\n autoPlayEnabled: scene.autoPlayEnabled ?? false,\n // Playback settings\n autoplaySpeed: scene.autoplaySpeed,\n loopMode: scene.loopMode,\n\n // XR Configuration\n includeXR: scene.includeXR,\n xrMode: scene.xrMode,\n\n // Custom Script\n customScript: scene.customScript,\n\n // Template type (default to standard)\n templateType: 'standard',\n\n // Splat Swap System\n additionalSplats: scene.additionalSplats || [],\n keepMeshesInMemory: scene.keepMeshesInMemory ?? false\n };\n\n return exportProps;\n}\n\n/**\n * Viewer configuration for dynamic viewer runtime\n */\nexport interface ViewerConfig {\n splatUrl: string;\n sogUrl?: string;\n /** LOD streaming format URL (lod-meta.json) - highest priority for loading */\n lodMetaUrl?: string;\n fallbackUrls?: string[];\n scale?: { x: number; y: number; z: number };\n position: number[];\n rotation: number[];\n invertXScale?: boolean;\n invertYScale?: boolean;\n waypoints?: any[];\n hotspots?: any[];\n portals?: any[];\n /** Nested skybox config (newer format) */\n skybox?: { url: string; rotation?: number; intensity?: number; enableIBL?: boolean };\n /** Flat skybox URL (older format, for backwards compatibility) */\n skyboxUrl?: string;\n /** Flat skybox rotation in radians (older format, for backwards compatibility) */\n skyboxRotation?: number;\n customMeshes?: any[];\n htmlMeshes?: any[];\n lights?: any[];\n particles?: any[];\n collisionMeshesData?: any[];\n cameraMode?: string;\n defaultCameraMode?: string;\n allowedCameraModes?: string[];\n autoPlay?: boolean;\n uiColor?: string;\n uiOptions?: any;\n // Camera settings (matching HTML export CONFIG)\n fov?: number;\n nearClip?: number;\n farClip?: number;\n playerHeight?: number;\n cameraMovementSpeed?: number;\n cameraRotationSensitivity?: number;\n // Playback settings\n /** Speed of autoplay in progress per second (default: calculated from waypoint durations) */\n autoplaySpeed?: number;\n /** Loop behavior for playback: 'loop' restarts at beginning, 'pingpong' reverses direction, 'none' stops at end */\n loopMode?: 'loop' | 'pingpong' | 'none';\n // Splat Swap System\n additionalSplats?: SplatSwapPoint[];\n keepMeshesInMemory?: boolean;\n // XR Configuration\n includeXR?: boolean;\n xrMode?: 'ar' | 'vr' | 'both';\n // Custom Script\n customScript?: string;\n // Background color\n backgroundColor?: string;\n}\n\n/**\n * Transform ExportProps to scene config for dynamic viewer\n * This is a simpler format for runtime use\n */\nexport function exportPropsToViewerConfig(props: ExportProps): ViewerConfig {\n // Get FOV from first waypoint if available, otherwise default to 60\n const fov = props.waypoints?.[0]?.fov || 60;\n\n return {\n splatUrl: props.splatUrl,\n sogUrl: props.sogUrl,\n lodMetaUrl: props.lodMetaUrl, // LOD streaming format (highest priority)\n fallbackUrls: props.fallbackUrls,\n scale: props.scale,\n position: props.splatPosition || [0, 0, 0],\n rotation: props.splatRotation || [0, 0, 0],\n invertXScale: (props as any).invertXScale,\n invertYScale: props.invertYScale,\n waypoints: props.waypoints,\n hotspots: props.hotspots,\n portals: props.portals,\n skybox: props.skybox,\n customMeshes: props.customMeshes,\n htmlMeshes: props.htmlMeshes,\n lights: props.lights,\n particles: props.particles,\n collisionMeshesData: props.collisionMeshesData,\n cameraMode: props.defaultCameraMode,\n defaultCameraMode: props.defaultCameraMode,\n allowedCameraModes: props.allowedCameraModes,\n autoPlay: props.autoPlayEnabled,\n uiColor: props.uiColor,\n uiOptions: props.uiOptions,\n // Camera settings (matching HTML export CONFIG)\n fov: fov,\n nearClip: (props as any).minClipPlane || 0.1,\n farClip: (props as any).maxClipPlane || 1000,\n playerHeight: props.playerHeight || 1.6,\n cameraMovementSpeed: props.cameraMovementSpeed || 1,\n cameraRotationSensitivity: props.cameraRotationSensitivity || 0.2,\n // Playback settings\n autoplaySpeed: (props as any).autoplaySpeed,\n loopMode: (props as any).loopMode,\n // Splat Swap System\n additionalSplats: props.additionalSplats,\n keepMeshesInMemory: props.keepMeshesInMemory ?? false,\n // XR Configuration\n includeXR: props.includeXR,\n xrMode: props.xrMode,\n // Custom Script\n customScript: props.customScript\n };\n}\n","/**\n * Dynamic Viewer UI Module\n *\n * Generates and injects UI elements and CSS for the dynamic viewer.\n * Uses MINIMAL theme for embedded viewers.\n */\n\nimport type { ViewerConfig, ButtonLabels } from '../types';\n\n/**\n * Default button labels for internationalization (i18n)\n */\nconst DEFAULT_BUTTON_LABELS: Required<ButtonLabels> = {\n tour: 'Tour',\n explore: 'Explore',\n hybrid: 'Hybrid',\n walk: 'Walk',\n orbit: 'Orbit',\n next: 'Next',\n previous: 'Prev',\n startExperience: 'Start Experience',\n returnToTour: 'Return to Tour',\n fullscreen: 'Fullscreen',\n mute: 'Mute',\n unmute: 'Unmute',\n helpTitle: 'Controls & Help',\n percentageFormat: '{n}%'\n};\n\n/**\n * Get a button label, falling back to default if not provided\n */\nfunction getButtonLabel(labels: ButtonLabels | undefined, key: keyof ButtonLabels): string {\n return labels?.[key] || DEFAULT_BUTTON_LABELS[key];\n}\n\n/**\n * Format a percentage label using the configured format\n */\nfunction formatPercentageLabel(labels: ButtonLabels | undefined, value: number): string {\n const format = labels?.percentageFormat || DEFAULT_BUTTON_LABELS.percentageFormat;\n return format.replace('{n}', String(value));\n}\n\nexport interface UIOptions {\n uiColor?: string;\n showScrollControls?: boolean;\n showModeToggle?: boolean;\n showFullscreenButton?: boolean;\n showHelpButton?: boolean;\n showPreloader?: boolean;\n allowedCameraModes?: string[];\n defaultCameraMode?: string;\n customPreloaderLogoUrl?: string;\n /** Custom button labels for internationalization (i18n) */\n buttonLabels?: ButtonLabels;\n /** Whether to hide the watermark (whitelabeling) */\n hideWatermark?: boolean;\n /** Custom watermark text (whitelabeling) */\n watermarkText?: string;\n /** Custom watermark link URL (whitelabeling) */\n watermarkLink?: string;\n /** Scene ID for default watermark link */\n sceneId?: string;\n}\n\nexport interface UIElements {\n preloader?: HTMLElement;\n scrollControls?: HTMLElement;\n modeToggle?: HTMLElement;\n fullscreenButton?: HTMLElement;\n helpButton?: HTMLElement;\n helpPanel?: HTMLElement;\n waypointInfo?: HTMLElement;\n progressBar?: HTMLElement;\n progressText?: HTMLElement;\n hotspotPopup?: HTMLElement;\n joystick?: HTMLElement;\n joystickThumb?: HTMLElement;\n lookZone?: HTMLElement;\n cameraModeToggle?: HTMLElement;\n returnWaypointButton?: HTMLElement;\n vrButton?: HTMLElement;\n arButton?: HTMLElement;\n watermark?: HTMLElement;\n}\n\n/**\n * Generate MINIMAL CSS styles for the viewer UI\n */\nexport function generateViewerStyles(uiColor: string = '#4CAF50'): string {\n return `\n /* Container - CRITICAL: must be position relative and contained */\n .storysplat-viewer-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-family: system-ui, -apple-system, sans-serif;\n box-sizing: border-box;\n }\n\n .storysplat-viewer-container * {\n box-sizing: border-box;\n }\n\n .storysplat-viewer-container canvas {\n position: absolute;\n top: 0;\n left: 0;\n width: 100% !important;\n height: 100% !important;\n display: block;\n touch-action: none;\n }\n\n /* Preloader - semi-transparent to see loading/reveal effect */\n .storysplat-preloader {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(30, 30, 30, 0.85);\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n z-index: 100000;\n transition: opacity 0.5s ease-out;\n }\n\n .storysplat-preloader.hidden {\n opacity: 0;\n pointer-events: none;\n }\n\n .storysplat-preloader-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 40px;\n gap: 20px;\n }\n\n .storysplat-preloader-media {\n display: flex;\n align-items: center;\n gap: 40px;\n }\n\n .storysplat-preloader-image {\n height: 200px;\n width: auto;\n object-fit: contain;\n }\n\n .storysplat-preloader-image-inverted {\n height: 200px;\n width: auto;\n object-fit: contain;\n filter: invert(1);\n margin-right: -100px;\n }\n\n .storysplat-preloader-lottie {\n height: 200px;\n width: 200px;\n }\n\n @media (max-width: 768px) {\n .storysplat-preloader-image,\n .storysplat-preloader-image-inverted {\n height: 100px;\n }\n .storysplat-preloader-image-inverted {\n margin-right: -50px;\n }\n .storysplat-preloader-lottie {\n height: 100px;\n width: 100px;\n }\n }\n\n .storysplat-preloader-progress {\n width: 200px;\n height: 4px;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 2px;\n margin-top: 20px;\n position: relative;\n }\n\n .storysplat-preloader-bar {\n width: 0%;\n height: 100%;\n background: ${uiColor};\n border-radius: 2px;\n transition: width 0.3s ease-out;\n }\n\n .storysplat-preloader-text {\n position: absolute;\n top: -25px;\n left: 50%;\n transform: translateX(-50%);\n color: white;\n font-size: 14px;\n white-space: nowrap;\n }\n\n /* Minimal Scroll Controls */\n .storysplat-scroll-controls {\n position: absolute;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: 150px;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n z-index: 1000;\n }\n\n .storysplat-scroll-content {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n }\n\n .storysplat-progress-text {\n font-size: 14px;\n color: white;\n font-variant-numeric: tabular-nums;\n min-width: 40px;\n text-align: center;\n }\n\n .storysplat-progress-container {\n width: 90%;\n max-width: 150px;\n height: 3px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 2px;\n overflow: hidden;\n }\n\n .storysplat-progress-bar {\n height: 100%;\n background: ${uiColor};\n transition: width 0.3s ease-out;\n width: 0%;\n }\n\n .storysplat-scroll-buttons {\n display: flex;\n justify-content: center;\n gap: 5px;\n z-index: 1000;\n }\n\n /* Minimal Button Style */\n .storysplat-btn {\n background: rgba(0, 0, 0, 0.3);\n border: none;\n color: white;\n padding: 4px 8px;\n font-size: 12px;\n border-radius: 3px;\n cursor: pointer;\n transition: background 0.2s;\n }\n\n .storysplat-btn:hover {\n background: rgba(0, 0, 0, 0.5);\n }\n\n .storysplat-btn-play {\n background: rgba(0, 0, 0, 0.3);\n border: none;\n color: white;\n padding: 4px 8px;\n border-radius: 3px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .storysplat-btn-play svg {\n width: 12px;\n height: 12px;\n fill: white;\n }\n\n /* Mode Toggle - Minimal */\n .storysplat-mode-container {\n margin-top: 5px;\n }\n\n .storysplat-mode-toggle {\n display: flex;\n gap: 5px;\n }\n\n .storysplat-mode-btn {\n background: rgba(0, 0, 0, 0.3);\n border: none;\n color: white;\n padding: 3px 6px;\n font-size: 11px;\n border-radius: 2px;\n cursor: pointer;\n transition: all 0.2s ease;\n }\n\n .storysplat-mode-btn.selected {\n background: ${uiColor} !important;\n }\n\n .storysplat-mode-btn:hover {\n background: rgba(0, 0, 0, 0.5);\n }\n\n /* Fullscreen Button - Minimal */\n .storysplat-fullscreen-btn {\n position: absolute;\n top: 10px;\n right: 10px;\n background: rgba(0, 0, 0, 0);\n border: none;\n padding: 5px;\n border-radius: 3px;\n cursor: pointer;\n z-index: 1000;\n }\n\n .storysplat-fullscreen-btn svg {\n width: 20px;\n height: 20px;\n fill: white;\n }\n\n /* Help Button - Minimal */\n .storysplat-help-btn {\n position: absolute;\n top: 10px;\n left: 10px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.3);\n color: white;\n border: none;\n cursor: pointer;\n font-size: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n transition: all 0.2s ease;\n }\n\n .storysplat-help-btn:hover {\n background: rgba(0, 0, 0, 0.5);\n }\n\n .storysplat-help-panel {\n position: absolute;\n top: 50px;\n left: 10px;\n background: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 15px;\n border-radius: 5px;\n max-width: 280px;\n z-index: 999;\n display: none;\n font-size: 12px;\n }\n\n .storysplat-help-panel.visible {\n display: block;\n }\n\n /* XR (VR/AR) Buttons - Minimal */\n .storysplat-xr-btn {\n position: absolute;\n top: 10px;\n right: 45px;\n background: rgba(0, 0, 0, 0.5);\n border: none;\n padding: 6px 10px;\n border-radius: 4px;\n cursor: pointer;\n z-index: 1000;\n color: white;\n font-size: 11px;\n font-weight: bold;\n text-transform: uppercase;\n transition: all 0.2s ease;\n display: none;\n }\n\n .storysplat-xr-btn.available {\n display: block;\n }\n\n .storysplat-xr-btn.active {\n background: ${uiColor};\n }\n\n .storysplat-xr-btn:hover {\n background: rgba(0, 0, 0, 0.7);\n }\n\n .storysplat-xr-btn.active:hover {\n background: ${uiColor};\n opacity: 0.9;\n }\n\n .storysplat-ar-btn {\n right: 85px;\n }\n\n .storysplat-help-panel h3 {\n margin: 0 0 10px 0;\n font-size: 14px;\n font-weight: 600;\n }\n\n .storysplat-help-panel p {\n margin: 5px 0;\n line-height: 1.4;\n }\n\n /* Waypoint Info - Top Banner Style (matching BabylonJS minimal export) */\n .storysplat-waypoint-info {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n background: rgba(0, 0, 0, 0.5);\n color: white;\n text-align: center;\n z-index: 1000;\n pointer-events: none;\n display: none;\n }\n\n .storysplat-waypoint-info.hasContent {\n display: block;\n padding: 50px 30px 30px 30px;\n }\n\n .storysplat-waypoint-info h2 {\n margin: 0 0 8px 0;\n font-size: 18px;\n font-weight: bold;\n }\n\n .storysplat-waypoint-info p {\n margin: 0;\n font-size: 14px;\n line-height: 1.5;\n opacity: 0.9;\n }\n\n /* Responsive waypoint info */\n @media (max-height: 600px) {\n .storysplat-waypoint-info.hasContent {\n padding: 20px 20px 15px 20px;\n font-size: 13px;\n }\n }\n\n @media (max-height: 500px) {\n .storysplat-waypoint-info.hasContent {\n padding: 10px 15px 10px 15px;\n font-size: 12px;\n }\n }\n\n /* Hotspot Popup - Matching BabylonJS HTML export styles */\n .storysplat-hotspot-popup {\n position: fixed;\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 20px;\n border-radius: 10px;\n z-index: 100001;\n box-shadow: 0 0 10px rgba(0,0,0,0.5);\n display: none;\n font-size: 14px;\n flex-direction: column;\n font-family: system-ui, -apple-system, sans-serif;\n /* Default centered positioning */\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n max-width: 80%;\n max-height: 80vh;\n overflow: auto;\n }\n\n .storysplat-hotspot-popup.visible {\n display: flex;\n }\n\n /* Fullscreen mode for click activation with image/iframe content */\n .storysplat-hotspot-popup.fullscreen {\n top: 0 !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n transform: none !important;\n width: 100% !important;\n height: 100% !important;\n max-width: 100% !important;\n max-height: 100% !important;\n margin: 0 !important;\n border-radius: 0 !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n flex-direction: column !important;\n padding: 0 !important;\n }\n\n .storysplat-hotspot-popup h2,\n .storysplat-hotspot-popup-title {\n margin: 0 0 10px 0;\n font-size: 18px;\n font-weight: 600;\n padding-right: 30px;\n }\n\n .storysplat-hotspot-popup p {\n margin: 0 0 10px 0;\n line-height: 1.5;\n font-size: 14px;\n white-space: pre-wrap;\n }\n\n .storysplat-hotspot-popup img {\n max-width: 100%;\n height: auto;\n border-radius: 5px;\n margin-bottom: 10px;\n display: block;\n }\n\n .storysplat-hotspot-popup.fullscreen img {\n width: 80% !important;\n height: 80vh !important;\n max-width: 80% !important;\n object-fit: contain !important;\n border: 2px solid rgba(255, 255, 255);\n margin: 30px 0;\n }\n\n .storysplat-hotspot-popup iframe {\n width: 100%;\n height: 400px;\n border: none;\n border-radius: 5px;\n margin-bottom: 10px;\n }\n\n .storysplat-hotspot-popup.fullscreen iframe {\n width: 80% !important;\n height: 80vh !important;\n margin: 30px 0;\n }\n\n .storysplat-hotspot-popup video {\n max-width: 100%;\n height: auto;\n border-radius: 5px;\n margin-bottom: 10px;\n display: block;\n }\n\n .storysplat-hotspot-popup.fullscreen video {\n width: 80% !important;\n height: 80vh !important;\n max-width: 80% !important;\n object-fit: contain !important;\n border: 2px solid rgba(255, 255, 255);\n margin: 30px 0;\n }\n\n /* Close button - matching BabylonJS export (green button at bottom) */\n .storysplat-hotspot-popup-close {\n width: auto;\n padding: 10px 20px;\n background-color: #4CAF50;\n border: none;\n color: white;\n cursor: pointer;\n border-radius: 5px;\n margin: 10px 0 0 0;\n font-size: 14px;\n font-weight: 500;\n transition: background-color 0.2s;\n }\n\n .storysplat-hotspot-popup-close:hover {\n background-color: #45a049;\n }\n\n .storysplat-hotspot-popup-link {\n display: block;\n padding: 8px 16px;\n background: #007bff;\n color: white;\n text-decoration: none;\n border-radius: 4px;\n text-align: center;\n font-weight: 500;\n margin: 0 0 10px 0;\n cursor: pointer;\n transition: background-color 0.2s;\n }\n\n .storysplat-hotspot-popup-link:hover {\n background: #0056b3;\n }\n\n /* Mobile Responsive */\n @media (max-width: 768px) {\n .storysplat-scroll-controls {\n margin-bottom: 10px;\n }\n }\n\n @media (max-width: 540px) {\n .storysplat-scroll-controls {\n margin-bottom: 45px;\n }\n }\n\n /* Virtual Joystick Overlay - visual indicator only, touches pass through to canvas */\n .storysplat-joystick-container {\n position: absolute;\n bottom: 80px;\n left: 40px;\n width: 120px;\n height: 120px;\n z-index: 2000;\n pointer-events: none;\n touch-action: none;\n display: none;\n }\n\n .storysplat-joystick-container.visible {\n display: block;\n }\n\n .storysplat-joystick-base {\n position: absolute;\n top: 0;\n left: 0;\n width: 120px;\n height: 120px;\n background: rgba(255, 255, 255, 0.2);\n border: 3px solid rgba(255, 255, 255, 0.4);\n border-radius: 50%;\n pointer-events: none;\n }\n\n .storysplat-joystick-thumb {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 50px;\n height: 50px;\n margin-left: -25px;\n margin-top: -25px;\n background: rgba(255, 255, 255, 0.6);\n border: 3px solid rgba(255, 255, 255, 0.8);\n border-radius: 50%;\n pointer-events: none;\n transition: transform 0.05s ease-out;\n }\n\n .storysplat-joystick-thumb.active {\n background: rgba(255, 255, 255, 0.8);\n border-color: white;\n }\n\n /* Look Zone Indicator (right side) */\n .storysplat-look-zone {\n position: absolute;\n bottom: 80px;\n right: 40px;\n width: 100px;\n height: 100px;\n z-index: 1000;\n pointer-events: none;\n display: none;\n }\n\n .storysplat-look-zone.visible {\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .storysplat-look-zone-icon {\n width: 60px;\n height: 60px;\n background: rgba(255, 255, 255, 0.15);\n border: 2px solid rgba(255, 255, 255, 0.25);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .storysplat-look-zone-icon svg {\n width: 30px;\n height: 30px;\n fill: rgba(255, 255, 255, 0.5);\n }\n\n /* Camera Mode Toggle (Orbit/Fly) - Mobile only in explore mode */\n .storysplat-camera-mode-toggle {\n position: absolute;\n bottom: 210px;\n left: 40px;\n width: 44px;\n height: 44px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.5);\n border: 2px solid rgba(255, 255, 255, 0.3);\n cursor: pointer;\n z-index: 2001;\n display: none;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n\n .storysplat-camera-mode-toggle.visible {\n display: flex;\n }\n\n .storysplat-camera-mode-toggle:active {\n background: rgba(0, 0, 0, 0.7);\n transform: scale(0.95);\n }\n\n .storysplat-camera-mode-toggle svg {\n width: 24px;\n height: 24px;\n fill: white;\n }\n\n .storysplat-camera-mode-toggle .orbit-icon,\n .storysplat-camera-mode-toggle .fly-icon {\n display: none;\n }\n\n .storysplat-camera-mode-toggle.orbit .orbit-icon {\n display: block;\n }\n\n .storysplat-camera-mode-toggle.fly .fly-icon {\n display: block;\n }\n\n /* Return to Waypoint Button - Explore mode only */\n .storysplat-return-waypoint-btn {\n position: absolute;\n bottom: 20px;\n left: 20px;\n padding: 10px 16px;\n background: rgba(0, 0, 0, 0.6);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 8px;\n color: white;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n z-index: 2001;\n display: none;\n align-items: center;\n gap: 8px;\n transition: all 0.2s ease;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n }\n\n .storysplat-return-waypoint-btn.visible {\n display: flex;\n }\n\n .storysplat-return-waypoint-btn:hover {\n background: rgba(0, 0, 0, 0.8);\n border-color: rgba(255, 255, 255, 0.5);\n }\n\n .storysplat-return-waypoint-btn:active {\n transform: scale(0.98);\n }\n\n .storysplat-return-waypoint-btn svg {\n width: 16px;\n height: 16px;\n fill: white;\n }\n\n @media (max-width: 768px) {\n .storysplat-return-waypoint-btn {\n bottom: auto;\n top: 60px;\n left: 10px;\n padding: 8px 12px;\n font-size: 12px;\n }\n }\n\n /* Lazy Load Container */\n .storysplat-lazy-load-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background: #1a1a1a;\n z-index: 10001;\n }\n\n .storysplat-lazy-load-thumbnail {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 0.7;\n }\n\n .storysplat-lazy-load-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: linear-gradient(\n to bottom,\n rgba(0, 0, 0, 0.3) 0%,\n rgba(0, 0, 0, 0.5) 50%,\n rgba(0, 0, 0, 0.7) 100%\n );\n }\n\n .storysplat-lazy-load-content {\n position: relative;\n z-index: 1;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n }\n\n .storysplat-lazy-load-start-btn {\n padding: 16px 32px;\n background: ${uiColor};\n border: none;\n border-radius: 50px;\n color: white;\n font-size: 18px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n transition: all 0.3s ease;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n }\n\n .storysplat-lazy-load-start-btn:hover {\n transform: scale(1.05);\n box-shadow: 0 6px 30px rgba(0, 0, 0, 0.4);\n }\n\n .storysplat-lazy-load-start-btn:active {\n transform: scale(0.98);\n }\n\n .storysplat-lazy-load-start-btn svg {\n width: 24px;\n height: 24px;\n fill: white;\n }\n\n @media (max-width: 768px) {\n .storysplat-lazy-load-start-btn {\n padding: 12px 24px;\n font-size: 16px;\n }\n }\n\n /* Watermark - matches BabylonJS HTML export */\n .storysplat-watermark {\n position: fixed;\n bottom: 10px;\n right: 10px;\n background-color: rgba(0, 0, 0, 0.5);\n color: white;\n padding: 5px 10px;\n border-radius: 5px;\n font-size: 12px;\n z-index: 1000;\n pointer-events: auto; /* Allow pointer events for the entire watermark so links work */\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n cursor: default;\n }\n\n .storysplat-watermark a {\n color: ${uiColor};\n text-decoration: none;\n cursor: pointer;\n }\n\n .storysplat-watermark a:hover {\n text-decoration: underline;\n }\n `;\n}\n\n/**\n * Inject CSS styles into the document\n */\nexport function injectStyles(uiColor: string = '#4CAF50'): HTMLStyleElement {\n const existingStyle = document.getElementById('storysplat-viewer-styles');\n if (existingStyle) {\n existingStyle.remove();\n }\n\n const style = document.createElement('style');\n style.id = 'storysplat-viewer-styles';\n style.textContent = generateViewerStyles(uiColor);\n document.head.appendChild(style);\n return style;\n}\n\n// StorySplat default assets\nconst STORYSPLAT_LOGO_URL = 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fimages%2FStorySplat.webp?alt=media&token=953e8ab3-1865-4ac1-a98d-b548b7066bda';\nconst STORYSPLAT_LOTTIE_URL = 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Flotties%2FstorySplatLottie.json?alt=media&token=d7edc19d-9cb8-4c6e-a94c-cba1d2b65d5e';\n\n/**\n * Inject Lottie player script if not already loaded\n */\nfunction ensureLottiePlayer(): Promise<void> {\n return new Promise((resolve) => {\n // Check if already loaded\n if (customElements.get('lottie-player')) {\n resolve();\n return;\n }\n\n // Check if script is already being loaded\n const existingScript = document.querySelector('script[src*=\"lottie-player\"]');\n if (existingScript) {\n existingScript.addEventListener('load', () => resolve());\n return;\n }\n\n // Load the script\n const script = document.createElement('script');\n script.src = 'https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js';\n script.onload = () => resolve();\n document.head.appendChild(script);\n });\n}\n\n/**\n * Create preloader element\n */\nexport function createPreloader(container: HTMLElement, customLogoUrl?: string): HTMLElement {\n const preloader = document.createElement('div');\n preloader.className = 'storysplat-preloader';\n\n const useCustomLogo = !!customLogoUrl;\n\n preloader.innerHTML = `\n <div class=\"storysplat-preloader-content\">\n <div class=\"storysplat-preloader-media\">\n ${useCustomLogo\n ? `<img class=\"storysplat-preloader-image\" src=\"${customLogoUrl}\" alt=\"Custom Logo\" />`\n : `<img class=\"storysplat-preloader-image-inverted\" src=\"${STORYSPLAT_LOGO_URL}\" alt=\"StorySplat Logo\" />`\n }\n ${!useCustomLogo\n ? `<lottie-player class=\"storysplat-preloader-lottie\"\n src=\"${STORYSPLAT_LOTTIE_URL}\"\n background=\"transparent\"\n speed=\"1\"\n loop\n autoplay>\n </lottie-player>`\n : ''\n }\n </div>\n <div class=\"storysplat-preloader-progress\">\n <div class=\"storysplat-preloader-text\">Loading... 0%</div>\n <div class=\"storysplat-preloader-bar\"></div>\n </div>\n </div>\n `;\n container.appendChild(preloader);\n\n // Load Lottie player script if using default logo\n if (!useCustomLogo) {\n ensureLottiePlayer();\n }\n\n return preloader;\n}\n\n/**\n * Update preloader progress\n */\nexport function updatePreloaderProgress(preloader: HTMLElement, progress: number, text?: string): void {\n const bar = preloader.querySelector('.storysplat-preloader-bar') as HTMLElement;\n const textEl = preloader.querySelector('.storysplat-preloader-text') as HTMLElement;\n const percent = Math.max(0, Math.min(100, progress * 100));\n if (bar) bar.style.width = `${percent}%`;\n if (textEl) textEl.textContent = text || `Loading... ${Math.round(percent)}%`;\n}\n\n/**\n * Hide preloader\n */\nexport function hidePreloader(preloader: HTMLElement): void {\n preloader.classList.add('hidden');\n setTimeout(() => preloader.remove(), 500);\n}\n\n/**\n * Create UI elements for the viewer\n */\nexport function createUIElements(\n container: HTMLElement,\n config: ViewerConfig,\n options: UIOptions = {}\n): UIElements {\n const {\n uiColor = '#4CAF50',\n showScrollControls = true,\n showModeToggle = true,\n showFullscreenButton = true,\n showHelpButton = false, // Minimal: hide by default\n showPreloader = true,\n allowedCameraModes = ['tour', 'explore'],\n defaultCameraMode = 'tour',\n customPreloaderLogoUrl,\n buttonLabels,\n hideWatermark = false,\n watermarkText,\n watermarkLink,\n sceneId\n } = options;\n\n // Get localized labels\n const labels = {\n tour: getButtonLabel(buttonLabels, 'tour'),\n explore: getButtonLabel(buttonLabels, 'explore'),\n walk: getButtonLabel(buttonLabels, 'walk'),\n previous: getButtonLabel(buttonLabels, 'previous'),\n next: getButtonLabel(buttonLabels, 'next'),\n helpTitle: getButtonLabel(buttonLabels, 'helpTitle'),\n returnToTour: getButtonLabel(buttonLabels, 'returnToTour'),\n };\n\n const elements: UIElements = {};\n\n // Clean up any existing UI elements from previous renders\n container.querySelectorAll('.storysplat-preloader, .storysplat-scroll-controls, .storysplat-waypoint-info, .storysplat-fullscreen-btn, .storysplat-help-btn, .storysplat-help-panel, .storysplat-watermark').forEach(el => el.remove());\n\n // Inject styles\n injectStyles(uiColor);\n\n // Add container class\n container.classList.add('storysplat-viewer-container');\n\n // Create Preloader\n if (showPreloader) {\n elements.preloader = createPreloader(container, customPreloaderLogoUrl);\n }\n\n // Create Waypoint Info Panel (top banner style)\n const waypointInfo = document.createElement('div');\n waypointInfo.className = 'storysplat-waypoint-info';\n waypointInfo.innerHTML = `\n <h2 class=\"storysplat-waypoint-title\"></h2>\n <p class=\"storysplat-waypoint-description\"></p>\n `;\n container.appendChild(waypointInfo);\n elements.waypointInfo = waypointInfo;\n\n // Create Scroll Controls\n if (showScrollControls && config.waypoints && config.waypoints.length > 0) {\n const scrollControls = document.createElement('div');\n scrollControls.className = 'storysplat-scroll-controls';\n scrollControls.innerHTML = `\n <div class=\"storysplat-scroll-content\">\n <div class=\"storysplat-progress-text\">0%</div>\n <div class=\"storysplat-progress-container\">\n <div class=\"storysplat-progress-bar\"></div>\n </div>\n <div class=\"storysplat-scroll-buttons\">\n <button class=\"storysplat-btn storysplat-btn-prev\">${labels.previous}</button>\n <button class=\"storysplat-btn storysplat-btn-play\">\n <svg viewBox=\"0 0 24 24\"><path d=\"M8 5v14l11-7z\"/></svg>\n </button>\n <button class=\"storysplat-btn storysplat-btn-next\">${labels.next}</button>\n </div>\n ${showModeToggle && allowedCameraModes.length > 1 ? `\n <div class=\"storysplat-mode-container\">\n <div class=\"storysplat-mode-toggle\">\n ${allowedCameraModes.includes('tour') ? `<button class=\"storysplat-mode-btn ${defaultCameraMode === 'tour' ? 'selected' : ''}\" data-mode=\"tour\">${labels.tour}</button>` : ''}\n ${allowedCameraModes.includes('explore') ? `<button class=\"storysplat-mode-btn ${defaultCameraMode === 'explore' ? 'selected' : ''}\" data-mode=\"explore\">${labels.explore}</button>` : ''}\n ${allowedCameraModes.includes('walk') ? `<button class=\"storysplat-mode-btn ${defaultCameraMode === 'walk' ? 'selected' : ''}\" data-mode=\"walk\">${labels.walk}</button>` : ''}\n </div>\n </div>\n ` : ''}\n </div>\n `;\n container.appendChild(scrollControls);\n elements.scrollControls = scrollControls;\n elements.progressBar = scrollControls.querySelector('.storysplat-progress-bar') as HTMLElement;\n elements.progressText = scrollControls.querySelector('.storysplat-progress-text') as HTMLElement;\n }\n\n // Create Fullscreen Button\n if (showFullscreenButton) {\n const fullscreenBtn = document.createElement('button');\n fullscreenBtn.className = 'storysplat-fullscreen-btn';\n fullscreenBtn.setAttribute('aria-label', 'Toggle Fullscreen');\n fullscreenBtn.innerHTML = `\n <svg class=\"storysplat-expand-icon\" viewBox=\"0 0 24 24\">\n <path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"/>\n </svg>\n <svg class=\"storysplat-compress-icon\" viewBox=\"0 0 24 24\" style=\"display: none;\">\n <path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\"/>\n </svg>\n `;\n container.appendChild(fullscreenBtn);\n elements.fullscreenButton = fullscreenBtn;\n }\n\n // Create VR Button (hidden by default, shown when VR is available)\n const vrBtn = document.createElement('button');\n vrBtn.className = 'storysplat-xr-btn storysplat-vr-btn';\n vrBtn.setAttribute('aria-label', 'Enter VR');\n vrBtn.textContent = 'VR';\n container.appendChild(vrBtn);\n elements.vrButton = vrBtn;\n\n // Create AR Button (hidden by default, shown when AR is available)\n const arBtn = document.createElement('button');\n arBtn.className = 'storysplat-xr-btn storysplat-ar-btn';\n arBtn.setAttribute('aria-label', 'Enter AR');\n arBtn.textContent = 'AR';\n container.appendChild(arBtn);\n elements.arButton = arBtn;\n\n // Create Help Button and Panel (optional in minimal)\n if (showHelpButton) {\n const helpBtn = document.createElement('button');\n helpBtn.className = 'storysplat-help-btn';\n helpBtn.setAttribute('title', 'Toggle Help');\n helpBtn.textContent = '?';\n container.appendChild(helpBtn);\n elements.helpButton = helpBtn;\n\n const helpPanel = document.createElement('div');\n helpPanel.className = 'storysplat-help-panel';\n helpPanel.innerHTML = `\n <h3>${labels.helpTitle}</h3>\n <p><strong>Camera Modes:</strong></p>\n <p>• ${labels.tour} - Follow predefined path</p>\n <p>• ${labels.explore} - Free movement</p>\n <p>• ${labels.walk} - First-person walking</p>\n <br/>\n <p><strong>${labels.tour} Mode:</strong></p>\n <p>• Scroll - Move along path</p>\n <p>• Drag - Look around</p>\n <br/>\n <p><strong>${labels.explore} Mode:</strong></p>\n <p>• LMB Drag - Orbit camera</p>\n <p>• RMB Drag - Fly/look</p>\n <p>• WASD/QE - Move camera</p>\n <p>• Shift - Move fast</p>\n <p>• Scroll/Pinch - Zoom</p>\n <p>• Double-click - Focus</p>\n <br/>\n <p><strong>${labels.walk} Mode:</strong></p>\n <p>• Click to lock mouse</p>\n <p>• WASD/Arrows - Move</p>\n <p>• Mouse - Look around</p>\n <p>• Shift - Sprint</p>\n <p>• Space - Jump</p>\n `;\n container.appendChild(helpPanel);\n elements.helpPanel = helpPanel;\n }\n\n // Create Hotspot Popup (always created, shown when needed)\n // Note: No overlay - matching BabylonJS export behavior\n const hotspotPopup = document.createElement('div');\n hotspotPopup.className = 'storysplat-hotspot-popup';\n hotspotPopup.id = 'hotspotContent'; // Match BabylonJS export ID\n hotspotPopup.innerHTML = `\n <h2 class=\"storysplat-hotspot-popup-title\"></h2>\n <div class=\"storysplat-hotspot-popup-content\"></div>\n <button class=\"storysplat-hotspot-popup-close\">Close</button>\n `;\n container.appendChild(hotspotPopup);\n elements.hotspotPopup = hotspotPopup;\n\n // Close popup on close button click\n const closeBtn = hotspotPopup.querySelector('.storysplat-hotspot-popup-close');\n closeBtn?.addEventListener('click', () => {\n hotspotPopup.classList.remove('visible', 'fullscreen');\n });\n\n // Create Virtual Joystick (for mobile explore mode)\n const joystick = document.createElement('div');\n joystick.className = 'storysplat-joystick-container';\n joystick.innerHTML = `\n <div class=\"storysplat-joystick-base\"></div>\n <div class=\"storysplat-joystick-thumb\"></div>\n `;\n container.appendChild(joystick);\n elements.joystick = joystick;\n elements.joystickThumb = joystick.querySelector('.storysplat-joystick-thumb') as HTMLElement;\n\n // Create Look Zone indicator (right side)\n const lookZone = document.createElement('div');\n lookZone.className = 'storysplat-look-zone';\n lookZone.innerHTML = `\n <div class=\"storysplat-look-zone-icon\">\n <svg viewBox=\"0 0 24 24\">\n <path d=\"M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z\"/>\n </svg>\n </div>\n `;\n container.appendChild(lookZone);\n elements.lookZone = lookZone;\n\n // Create Camera Mode Toggle (Orbit/Fly) for mobile explore mode\n const cameraModeToggle = document.createElement('button');\n cameraModeToggle.className = 'storysplat-camera-mode-toggle fly';\n cameraModeToggle.setAttribute('aria-label', 'Toggle camera mode');\n cameraModeToggle.innerHTML = `\n <svg class=\"orbit-icon\" viewBox=\"0 0 24 24\">\n <path d=\"M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z\"/>\n </svg>\n <svg class=\"fly-icon\" viewBox=\"0 0 24 24\">\n <path d=\"M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z\"/>\n </svg>\n `;\n container.appendChild(cameraModeToggle);\n elements.cameraModeToggle = cameraModeToggle;\n\n // Create Return to Waypoint button (shown in explore mode)\n const returnWaypointButton = document.createElement('button');\n returnWaypointButton.className = 'storysplat-return-waypoint-btn';\n returnWaypointButton.setAttribute('aria-label', labels.returnToTour);\n returnWaypointButton.innerHTML = `\n <svg viewBox=\"0 0 24 24\">\n <path d=\"M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z\"/>\n </svg>\n ${labels.returnToTour}\n `;\n container.appendChild(returnWaypointButton);\n elements.returnWaypointButton = returnWaypointButton;\n\n // Create Watermark (unless hidden for whitelabeling)\n if (!hideWatermark) {\n const watermark = document.createElement('div');\n watermark.className = 'storysplat-watermark';\n\n // Build watermark content - matches BabylonJS HTML export behavior\n const defaultWatermarkLink = sceneId\n ? `https://storysplat.com?ref=${sceneId}`\n : 'https://storysplat.com';\n const finalLink = watermarkLink || defaultWatermarkLink;\n\n if (watermarkText) {\n // Custom text provided (whitelabeling)\n watermark.innerHTML = `<a href=\"${finalLink}\" target=\"_blank\">${watermarkText}</a>`;\n } else {\n // Default StorySplat watermark\n watermark.innerHTML = `Created with <a href=\"${finalLink}\" target=\"_blank\">StorySplat</a>`;\n }\n\n container.appendChild(watermark);\n elements.watermark = watermark;\n }\n\n return elements;\n}\n\n/**\n * Connect UI elements to viewer controls\n */\nexport function connectUIToViewer(\n elements: UIElements,\n viewer: {\n nextWaypoint: () => void;\n prevWaypoint: () => void;\n play: () => void;\n pause: () => void;\n isPlaying: () => boolean;\n getCurrentWaypointIndex: () => number;\n getWaypointCount: () => number;\n getWaypoints?: () => any[];\n setCameraMode?: (mode: string) => void;\n on: (event: string, callback: (...args: any[]) => void) => void;\n },\n defaultCameraMode: string = 'tour',\n buttonLabels?: ButtonLabels\n): void {\n // Prev/Next buttons\n const prevBtn = elements.scrollControls?.querySelector('.storysplat-btn-prev');\n const nextBtn = elements.scrollControls?.querySelector('.storysplat-btn-next');\n const playBtn = elements.scrollControls?.querySelector('.storysplat-btn-play');\n\n if (prevBtn) {\n prevBtn.addEventListener('click', () => viewer.prevWaypoint());\n }\n\n if (nextBtn) {\n nextBtn.addEventListener('click', () => viewer.nextWaypoint());\n }\n\n if (playBtn) {\n // Update button icon based on state\n const updatePlayButtonIcon = (isPlaying: boolean) => {\n if (isPlaying) {\n playBtn.innerHTML = '<svg viewBox=\"0 0 24 24\"><path d=\"M6 4h4v16H6zm8 0h4v16h-4z\"/></svg>'; // Pause icon\n } else {\n playBtn.innerHTML = '<svg viewBox=\"0 0 24 24\"><path d=\"M8 5v14l11-7z\"/></svg>'; // Play icon\n }\n };\n\n playBtn.addEventListener('click', () => {\n if (viewer.isPlaying()) {\n viewer.pause();\n } else {\n viewer.play();\n }\n // Icon will be updated by event listeners below\n });\n\n // Listen for playback events to update button state (handles autoplay and programmatic play/pause)\n viewer.on('playbackStart', () => updatePlayButtonIcon(true));\n viewer.on('playbackStop', () => updatePlayButtonIcon(false));\n\n // Set initial state based on current playing status\n updatePlayButtonIcon(viewer.isPlaying());\n }\n\n // Help button\n if (elements.helpButton && elements.helpPanel) {\n elements.helpButton.addEventListener('click', () => {\n elements.helpPanel!.classList.toggle('visible');\n });\n }\n\n // Fullscreen button\n if (elements.fullscreenButton) {\n // Detect iOS - iOS does not support the Fullscreen API\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||\n (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // iPad Pro detection\n\n // Hide fullscreen button on iOS since the API doesn't work there\n if (isIOS) {\n elements.fullscreenButton.style.display = 'none';\n console.log('[StorySplat Viewer] Fullscreen button hidden on iOS (API not supported)');\n } else {\n const container = elements.fullscreenButton.parentElement;\n elements.fullscreenButton.addEventListener('click', () => {\n // Use webkit-prefixed API for Safari compatibility\n const doc = document as any;\n const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement;\n\n if (!fullscreenElement) {\n // Enter fullscreen - try standard API first, then webkit\n if (container?.requestFullscreen) {\n container.requestFullscreen();\n } else if ((container as any)?.webkitRequestFullscreen) {\n (container as any).webkitRequestFullscreen();\n }\n const expandIcon = elements.fullscreenButton!.querySelector('.storysplat-expand-icon') as HTMLElement;\n const compressIcon = elements.fullscreenButton!.querySelector('.storysplat-compress-icon') as HTMLElement;\n if (expandIcon) expandIcon.style.display = 'none';\n if (compressIcon) compressIcon.style.display = 'block';\n } else {\n // Exit fullscreen - try standard API first, then webkit\n if (doc.exitFullscreen) {\n doc.exitFullscreen();\n } else if (doc.webkitExitFullscreen) {\n doc.webkitExitFullscreen();\n }\n const expandIcon = elements.fullscreenButton!.querySelector('.storysplat-expand-icon') as HTMLElement;\n const compressIcon = elements.fullscreenButton!.querySelector('.storysplat-compress-icon') as HTMLElement;\n if (expandIcon) expandIcon.style.display = 'block';\n if (compressIcon) compressIcon.style.display = 'none';\n }\n });\n\n // Listen for fullscreen change events (including webkit prefix)\n const handleFullscreenChange = () => {\n const doc = document as any;\n const isFullscreen = doc.fullscreenElement || doc.webkitFullscreenElement;\n const expandIcon = elements.fullscreenButton!.querySelector('.storysplat-expand-icon') as HTMLElement;\n const compressIcon = elements.fullscreenButton!.querySelector('.storysplat-compress-icon') as HTMLElement;\n if (expandIcon) expandIcon.style.display = isFullscreen ? 'none' : 'block';\n if (compressIcon) compressIcon.style.display = isFullscreen ? 'block' : 'none';\n };\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n document.addEventListener('webkitfullscreenchange', handleFullscreenChange);\n }\n }\n\n // Helper to show/hide tour navigation controls based on mode\n const setTourControlsVisible = (visible: boolean) => {\n const progressText = elements.scrollControls?.querySelector('.storysplat-progress-text') as HTMLElement;\n const progressContainer = elements.scrollControls?.querySelector('.storysplat-progress-container') as HTMLElement;\n const scrollButtons = elements.scrollControls?.querySelector('.storysplat-scroll-buttons') as HTMLElement;\n\n const display = visible ? '' : 'none';\n if (progressText) progressText.style.display = display;\n if (progressContainer) progressContainer.style.display = display;\n if (scrollButtons) scrollButtons.style.display = display;\n };\n\n // Set initial visibility based on default mode\n setTourControlsVisible(defaultCameraMode === 'tour');\n\n // Mode toggle buttons\n if (viewer.setCameraMode) {\n const modeButtons = elements.scrollControls?.querySelectorAll('.storysplat-mode-btn');\n modeButtons?.forEach(btn => {\n btn.addEventListener('click', () => {\n const mode = btn.getAttribute('data-mode');\n if (mode && viewer.setCameraMode) {\n viewer.setCameraMode(mode);\n // Update selected state\n modeButtons.forEach(b => b.classList.remove('selected'));\n btn.classList.add('selected');\n // Show/hide tour controls based on mode\n setTourControlsVisible(mode === 'tour');\n }\n });\n });\n\n // Set initial visibility based on default mode\n viewer.on('modeChange', ({ mode }: { mode: string }) => {\n setTourControlsVisible(mode === 'tour');\n // Update button selected state\n modeButtons?.forEach(btn => {\n const btnMode = btn.getAttribute('data-mode');\n btn.classList.toggle('selected', btnMode === mode);\n });\n });\n }\n\n // Update progress continuously (throttled to prevent visual artifacts)\n let lastProgressUpdate = 0;\n let lastDisplayedPercentage = -1;\n\n viewer.on('progressUpdate', ({ progress }: { progress: number }) => {\n // Clamp progress to 0-1 range\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const percentage = clampedProgress * 100;\n const roundedPercentage = Math.round(percentage);\n\n // Throttle UI updates to ~30fps to prevent visual glitches\n const now = performance.now();\n if (now - lastProgressUpdate < 33) return;\n lastProgressUpdate = now;\n\n if (elements.progressBar) {\n elements.progressBar.style.width = `${percentage}%`;\n }\n\n // Only update text if the percentage actually changed (prevents flickering)\n if (elements.progressText && roundedPercentage !== lastDisplayedPercentage) {\n lastDisplayedPercentage = roundedPercentage;\n // Clear and set to ensure no duplicate content\n elements.progressText.innerHTML = '';\n elements.progressText.textContent = formatPercentageLabel(buttonLabels, roundedPercentage);\n }\n });\n\n // Update waypoint info panel when waypoint changes\n let lastDisplayedWaypointIndex = -1;\n viewer.on('waypointChange', ({ index, waypoint }: { index: number; waypoint?: any }) => {\n // Only update if waypoint actually changed\n if (index === lastDisplayedWaypointIndex) return;\n lastDisplayedWaypointIndex = index;\n\n if (!elements.waypointInfo) return;\n\n const titleEl = elements.waypointInfo.querySelector('.storysplat-waypoint-title') as HTMLElement;\n const descEl = elements.waypointInfo.querySelector('.storysplat-waypoint-description') as HTMLElement;\n\n if (!titleEl || !descEl) return;\n\n // Get waypoint data - either from event or from viewer\n const wp = waypoint || (viewer.getWaypoints?.()[index]);\n\n if (wp && (wp.name || wp.info)) {\n // Show waypoint name as title\n titleEl.textContent = wp.name || '';\n\n // Show waypoint info as description\n descEl.textContent = wp.info || '';\n\n // Show panel if there's content\n if (wp.name || wp.info) {\n elements.waypointInfo.classList.add('hasContent');\n } else {\n elements.waypointInfo.classList.remove('hasContent');\n }\n } else {\n // Hide panel if no content\n titleEl.textContent = '';\n descEl.textContent = '';\n elements.waypointInfo.classList.remove('hasContent');\n }\n });\n}\n\n/**\n * Show hotspot popup with content - matching BabylonJS HTML export behavior\n */\nexport function showHotspotPopup(container: HTMLElement, hotspot: any): void {\n const popup = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n\n if (!popup) return;\n\n const titleEl = popup.querySelector('.storysplat-hotspot-popup-title') as HTMLElement;\n const contentEl = popup.querySelector('.storysplat-hotspot-popup-content') as HTMLElement;\n const closeBtn = popup.querySelector('.storysplat-hotspot-popup-close') as HTMLElement;\n\n // Reset any previous custom styles and classes\n popup.style.cssText = '';\n popup.classList.remove('fullscreen');\n\n // Determine activation mode (default to 'click')\n const activationMode = hotspot.activationMode || 'click';\n const hasMediaContent = hotspot.photoUrl || hotspot.popupVideoUrl || (hotspot.contentType === 'iframe' && hotspot.iframeUrl);\n\n // Apply fullscreen mode for click activation with photo, video, or iframe (matching BabylonJS export)\n if (activationMode === 'click' && hasMediaContent) {\n popup.classList.add('fullscreen');\n }\n\n // Apply custom styling if provided (matching BabylonJS export)\n if (hotspot.backgroundColor) {\n popup.style.backgroundColor = hotspot.backgroundColor;\n }\n if (hotspot.textColor) {\n popup.style.color = hotspot.textColor;\n if (titleEl) titleEl.style.color = hotspot.textColor;\n }\n if (hotspot.fontFamily) {\n popup.style.fontFamily = hotspot.fontFamily;\n }\n if (hotspot.fontSize) {\n popup.style.fontSize = `${hotspot.fontSize}px`;\n }\n\n // Apply close button color if provided\n if (closeBtn && hotspot.closeButtonColor) {\n closeBtn.style.backgroundColor = hotspot.closeButtonColor;\n }\n\n // Set title\n if (titleEl) {\n titleEl.textContent = hotspot.title || 'Hotspot';\n }\n\n // Build content HTML - order matches BabylonJS export: title, iframe, photo, info, link\n let contentHtml = '';\n\n // Iframe content (before photo in BabylonJS export)\n if (hotspot.contentType === 'iframe' && hotspot.iframeUrl) {\n contentHtml += `<iframe src=\"${hotspot.iframeUrl}\" title=\"${hotspot.title || 'Embedded content'}\"></iframe>`;\n }\n\n // Video content (embedded video player in popup)\n if (hotspot.popupVideoUrl) {\n contentHtml += `<video src=\"${hotspot.popupVideoUrl}\" controls playsinline webkit-playsinline preload=\"metadata\"></video>`;\n }\n\n // Image content\n if (hotspot.photoUrl) {\n contentHtml += `<img src=\"${hotspot.photoUrl}\" alt=\"${hotspot.title || 'Hotspot image'}\" />`;\n }\n\n // Information text\n if (hotspot.information) {\n contentHtml += `<p>${hotspot.information}</p>`;\n }\n\n // External link\n if (hotspot.externalLinkUrl) {\n const btnColor = hotspot.externalLinkButtonColor || '#007bff';\n contentHtml += `\n <div onclick=\"window.open('${hotspot.externalLinkUrl}', '_blank', 'noopener,noreferrer')\"\n class=\"storysplat-hotspot-popup-link\" style=\"background-color: ${btnColor}\">\n ${hotspot.externalLinkText || 'Open External Link'}\n </div>\n `;\n }\n\n if (contentEl) {\n contentEl.innerHTML = contentHtml;\n }\n\n // Show popup (no overlay - matching BabylonJS export)\n popup.classList.add('visible');\n}\n\n/**\n * Hide hotspot popup\n */\nexport function hideHotspotPopup(container: HTMLElement): void {\n const popup = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n\n if (popup) popup.classList.remove('visible', 'fullscreen');\n}\n\n/**\n * Show/hide virtual joystick overlay\n */\nexport function setJoystickVisible(elements: UIElements, visible: boolean): void {\n if (elements.joystick) {\n if (visible) {\n elements.joystick.classList.add('visible');\n } else {\n elements.joystick.classList.remove('visible');\n }\n }\n if (elements.lookZone) {\n if (visible) {\n elements.lookZone.classList.add('visible');\n } else {\n elements.lookZone.classList.remove('visible');\n }\n }\n}\n\n/**\n * Update joystick thumb position based on touch input\n */\nexport function updateJoystickPosition(\n elements: UIElements,\n active: boolean,\n dx: number,\n dy: number,\n maxRadius: number\n): void {\n if (!elements.joystickThumb) return;\n\n if (active) {\n elements.joystickThumb.classList.add('active');\n // Clamp the offset to maxRadius\n const distance = Math.sqrt(dx * dx + dy * dy);\n const clampedDistance = Math.min(distance, maxRadius);\n const scale = distance > 0 ? clampedDistance / distance : 0;\n const clampedX = dx * scale;\n const clampedY = dy * scale;\n elements.joystickThumb.style.transform = `translate(${clampedX}px, ${clampedY}px)`;\n } else {\n elements.joystickThumb.classList.remove('active');\n elements.joystickThumb.style.transform = 'translate(0, 0)';\n }\n}\n\n/**\n * Show/hide camera mode toggle button (mobile-only, explore mode only)\n */\nexport function setCameraModeToggleVisible(elements: UIElements, visible: boolean): void {\n if (elements.cameraModeToggle) {\n if (visible) {\n elements.cameraModeToggle.classList.add('visible');\n } else {\n elements.cameraModeToggle.classList.remove('visible');\n }\n }\n}\n\n/**\n * Update camera mode toggle button to reflect current mode\n */\nexport function updateCameraModeToggle(elements: UIElements, mode: 'orbit' | 'fly'): void {\n if (elements.cameraModeToggle) {\n elements.cameraModeToggle.classList.remove('orbit', 'fly');\n elements.cameraModeToggle.classList.add(mode);\n }\n}\n\n/**\n * Show/hide return to waypoint button (explore mode only)\n */\nexport function setReturnWaypointButtonVisible(elements: UIElements, visible: boolean): void {\n if (elements.returnWaypointButton) {\n if (visible) {\n elements.returnWaypointButton.classList.add('visible');\n } else {\n elements.returnWaypointButton.classList.remove('visible');\n }\n }\n}\n\n/**\n * Lazy load UI options\n */\nexport interface LazyLoadUIOptions {\n thumbnailUrl?: string;\n buttonText?: string;\n uiColor?: string;\n onStart: () => void;\n}\n\n/**\n * Create lazy load UI with thumbnail and start button\n * Returns the container element for later removal\n */\nexport function createLazyLoadUI(\n container: HTMLElement,\n options: LazyLoadUIOptions\n): HTMLElement {\n const {\n thumbnailUrl,\n buttonText = 'Start Experience',\n uiColor = '#4CAF50',\n onStart\n } = options;\n\n // Inject styles if not already present\n injectStyles(uiColor);\n\n // Add container class\n container.classList.add('storysplat-viewer-container');\n\n // Create lazy load container\n const lazyLoadContainer = document.createElement('div');\n lazyLoadContainer.className = 'storysplat-lazy-load-container';\n\n // Build HTML content\n let html = '';\n\n // Thumbnail if provided\n if (thumbnailUrl) {\n html += `<img class=\"storysplat-lazy-load-thumbnail\" src=\"${thumbnailUrl}\" alt=\"Scene preview\" />`;\n }\n\n // Overlay gradient\n html += `<div class=\"storysplat-lazy-load-overlay\"></div>`;\n\n // Content with start button\n html += `\n <div class=\"storysplat-lazy-load-content\">\n <button class=\"storysplat-lazy-load-start-btn\" style=\"background: ${uiColor}\">\n <svg viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\"/>\n </svg>\n ${buttonText}\n </button>\n </div>\n `;\n\n lazyLoadContainer.innerHTML = html;\n container.appendChild(lazyLoadContainer);\n\n // Add click handler to start button\n const startBtn = lazyLoadContainer.querySelector('.storysplat-lazy-load-start-btn');\n startBtn?.addEventListener('click', () => {\n // Fade out and remove\n lazyLoadContainer.style.transition = 'opacity 0.3s ease-out';\n lazyLoadContainer.style.opacity = '0';\n setTimeout(() => {\n lazyLoadContainer.remove();\n onStart();\n }, 300);\n });\n\n return lazyLoadContainer;\n}\n","/**\n * CameraControls - PlayCanvas Camera Controller\n *\n * Ported from PlayCanvas engine scripts/esm/camera-controls.mjs\n * Provides orbit, fly, and focus camera modes with mouse, touch, and gamepad support.\n *\n * Controls:\n * - LMB drag: Orbit around focus point\n * - RMB drag + WASD: Fly mode\n * - Shift + drag / MMB: Pan\n * - Scroll / Pinch: Zoom\n * - Double-click: Focus on point (handled externally)\n */\n\nimport * as pc from 'playcanvas';\n\nconst tmpV1 = new pc.Vec3();\nconst tmpV2 = new pc.Vec3();\nconst pose = new pc.Pose();\nconst frame = new pc.InputFrame({\n move: [0, 0, 0],\n rotate: [0, 0, 0]\n});\n\n/**\n * Calculate the damp rate for smooth interpolation\n */\nexport const damp = (damping: number, dt: number): number => 1 - Math.pow(damping, dt * 1000);\n\n/**\n * Apply dead zone to gamepad stick input\n */\nconst applyDeadZone = (stick: number[], low: number, high: number): void => {\n const mag = Math.sqrt(stick[0] * stick[0] + stick[1] * stick[1]);\n if (mag < low) {\n stick.fill(0);\n return;\n }\n const scale = (mag - low) / (high - low);\n stick[0] *= scale / mag;\n stick[1] *= scale / mag;\n};\n\n/**\n * Converts screen space mouse deltas to world space pan vector\n */\nconst screenToWorld = (\n camera: pc.CameraComponent,\n dx: number,\n dy: number,\n dz: number,\n out: pc.Vec3 = new pc.Vec3()\n): pc.Vec3 => {\n const { fov, aspectRatio, horizontalFov, projection, orthoHeight } = camera;\n const app = (camera as any).system?.app;\n const { width, height } = app?.graphicsDevice?.clientRect || { width: 1920, height: 1080 };\n\n // normalize deltas to device coord space\n out.set(\n -(dx / width) * 2,\n (dy / height) * 2,\n 0\n );\n\n // calculate half size of the view frustum at the current distance\n const halfSize = tmpV2.set(0, 0, 0);\n if (projection === pc.PROJECTION_PERSPECTIVE) {\n const halfSlice = dz * Math.tan(0.5 * fov * pc.math.DEG_TO_RAD);\n if (horizontalFov) {\n halfSize.set(halfSlice, halfSlice / aspectRatio, 0);\n } else {\n halfSize.set(halfSlice * aspectRatio, halfSlice, 0);\n }\n } else {\n halfSize.set(orthoHeight * aspectRatio, orthoHeight, 0);\n }\n\n // scale by device coord space\n out.mul(halfSize);\n return out;\n};\n\ntype CameraMode = 'orbit' | 'fly' | 'focus';\ntype MobileInputLayout = 'joystick-joystick' | 'joystick-touch' | 'touch-joystick' | 'touch-touch';\n\nexport interface CameraControlsConfig {\n enableOrbit?: boolean;\n enableFly?: boolean;\n enablePan?: boolean;\n focusPoint?: pc.Vec3;\n moveSpeed?: number;\n moveFastSpeed?: number;\n moveSlowSpeed?: number;\n rotateSpeed?: number;\n rotateTouchSens?: number;\n rotateJoystickSens?: number;\n zoomSpeed?: number;\n zoomPinchSens?: number;\n focusDamping?: number;\n rotateDamping?: number;\n moveDamping?: number;\n zoomDamping?: number;\n pitchRange?: pc.Vec2;\n yawRange?: pc.Vec2;\n zoomRange?: pc.Vec2;\n gamepadDeadZone?: pc.Vec2;\n mobileInputLayout?: MobileInputLayout;\n}\n\n/**\n * CameraControls class - Manages camera with orbit, fly, and focus modes\n */\nexport class CameraControls {\n private camera: pc.Entity;\n private cameraComponent: pc.CameraComponent;\n private app: pc.Application;\n private enabled: boolean = true;\n\n // Mode state\n private _mode: CameraMode = 'orbit';\n private _enableOrbit: boolean = true;\n private _enableFly: boolean = true;\n enablePan: boolean = true;\n\n // Controllers\n private _flyController: pc.FlyController;\n private _orbitController: pc.OrbitController;\n private _focusController: pc.FocusController;\n private _controller: pc.InputController;\n private _pose: pc.Pose = new pc.Pose();\n\n // Input sources\n private _desktopInput: pc.KeyboardMouseSource;\n private _orbitMobileInput: pc.MultiTouchSource;\n private _flyMobileInput: pc.DualGestureSource;\n private _gamepadInput: pc.GamepadSource;\n\n // Zoom\n private _startZoomDist: number = 0;\n // Limit pitch to ±89° to avoid gimbal lock (world flip at ±90°)\n private _pitchRange: pc.Vec2 = new pc.Vec2(-89, 89);\n private _yawRange: pc.Vec2 = new pc.Vec2(-Infinity, Infinity);\n\n // Store the last focus point for orbit mode\n private _lastFocusPoint: pc.Vec3 = new pc.Vec3(0, 0, 0);\n private _zoomRange: pc.Vec2 = new pc.Vec2(0.01, Infinity);\n\n // Debug: track last yaw for discontinuity detection\n private _lastYaw?: number;\n\n // State tracking\n private _state = {\n axis: new pc.Vec3(),\n shift: 0,\n ctrl: 0,\n mouse: [0, 0, 0],\n touches: 0\n };\n\n // Speed settings\n moveSpeed: number = 25;\n moveFastSpeed: number = 50;\n moveSlowSpeed: number = 10;\n rotateSpeed: number = 0.05;\n rotateTouchSens: number = 0.1; // Touch sensitivity (10x lower than desktop)\n rotateJoystickSens: number = 1;\n zoomSpeed: number = 0.001;\n zoomPinchSens: number = 5;\n gamepadDeadZone: pc.Vec2 = new pc.Vec2(0.3, 0.6);\n\n // Joystick event name for UI\n joystickEventName: string = 'joystick';\n\n // Cleanup handlers\n private _destroyHandler: (() => void) | null = null;\n\n constructor(camera: pc.Entity, app: pc.Application, config: CameraControlsConfig = {}) {\n this.camera = camera;\n this.app = app;\n\n const cameraComponent = camera.camera;\n if (!cameraComponent) {\n throw new Error('CameraControls: camera component not found on entity');\n }\n this.cameraComponent = cameraComponent;\n\n // Initialize controllers\n this._flyController = new pc.FlyController();\n this._orbitController = new pc.OrbitController();\n this._focusController = new pc.FocusController();\n\n // Set orbit controller defaults\n this._orbitController.zoomRange = new pc.Vec2(0.01, Infinity);\n\n // Initialize input sources\n const canvas = app.graphicsDevice.canvas;\n this._desktopInput = new pc.KeyboardMouseSource();\n this._orbitMobileInput = new pc.MultiTouchSource();\n this._flyMobileInput = new pc.DualGestureSource();\n this._gamepadInput = new pc.GamepadSource();\n\n // Attach input sources to canvas\n this._desktopInput.attach(canvas);\n this._orbitMobileInput.attach(canvas);\n this._flyMobileInput.attach(canvas);\n this._gamepadInput.attach(canvas);\n\n // Expose UI joystick events\n this._flyMobileInput.on('joystick:position:left', ([bx, by, sx, sy]: number[]) => {\n if (this._mode !== 'fly') return;\n this.app.fire(`${this.joystickEventName}:left`, bx, by, sx, sy);\n });\n this._flyMobileInput.on('joystick:position:right', ([bx, by, sx, sy]: number[]) => {\n if (this._mode !== 'fly') return;\n this.app.fire(`${this.joystickEventName}:right`, bx, by, sx, sy);\n });\n\n // Initialize pose from camera position\n this._pose.look(this.camera.getPosition(), pc.Vec3.ZERO);\n\n // Set initial mode\n this._setMode('orbit');\n\n // Store controller reference\n this._controller = this._orbitController;\n\n // Apply config\n if (config.enableOrbit !== undefined) this.enableOrbit = config.enableOrbit;\n if (config.enableFly !== undefined) this.enableFly = config.enableFly;\n if (config.enablePan !== undefined) this.enablePan = config.enablePan;\n if (config.focusPoint) this.focusPoint = config.focusPoint;\n if (config.moveSpeed !== undefined) this.moveSpeed = config.moveSpeed;\n if (config.moveFastSpeed !== undefined) this.moveFastSpeed = config.moveFastSpeed;\n if (config.moveSlowSpeed !== undefined) this.moveSlowSpeed = config.moveSlowSpeed;\n if (config.rotateSpeed !== undefined) this.rotateSpeed = config.rotateSpeed;\n if (config.rotateTouchSens !== undefined) this.rotateTouchSens = config.rotateTouchSens;\n if (config.rotateJoystickSens !== undefined) this.rotateJoystickSens = config.rotateJoystickSens;\n if (config.zoomSpeed !== undefined) this.zoomSpeed = config.zoomSpeed;\n if (config.zoomPinchSens !== undefined) this.zoomPinchSens = config.zoomPinchSens;\n if (config.focusDamping !== undefined) this.focusDamping = config.focusDamping;\n if (config.rotateDamping !== undefined) this.rotateDamping = config.rotateDamping;\n if (config.moveDamping !== undefined) this.moveDamping = config.moveDamping;\n if (config.zoomDamping !== undefined) this.zoomDamping = config.zoomDamping;\n if (config.pitchRange) this.pitchRange = config.pitchRange;\n if (config.yawRange) this.yawRange = config.yawRange;\n if (config.zoomRange) this.zoomRange = config.zoomRange;\n if (config.gamepadDeadZone) this.gamepadDeadZone = config.gamepadDeadZone;\n if (config.mobileInputLayout) this.mobileInputLayout = config.mobileInputLayout;\n }\n\n // Enable/disable getters and setters\n set enableFly(enable: boolean) {\n this._enableFly = enable;\n if (!this._enableFly && this._mode === 'fly') {\n this._setMode('orbit');\n }\n }\n\n get enableFly(): boolean {\n return this._enableFly;\n }\n\n set enableOrbit(enable: boolean) {\n this._enableOrbit = enable;\n if (!this._enableOrbit && this._mode === 'orbit') {\n this._setMode('fly');\n }\n }\n\n get enableOrbit(): boolean {\n return this._enableOrbit;\n }\n\n // Damping getters/setters\n set focusDamping(damping: number) {\n this._focusController.focusDamping = damping;\n }\n\n get focusDamping(): number {\n return this._focusController.focusDamping;\n }\n\n set moveDamping(damping: number) {\n this._flyController.moveDamping = damping;\n }\n\n get moveDamping(): number {\n return this._flyController.moveDamping;\n }\n\n set rotateDamping(damping: number) {\n this._flyController.rotateDamping = damping;\n this._orbitController.rotateDamping = damping;\n }\n\n get rotateDamping(): number {\n return this._orbitController.rotateDamping;\n }\n\n set zoomDamping(damping: number) {\n this._orbitController.zoomDamping = damping;\n }\n\n get zoomDamping(): number {\n return this._orbitController.zoomDamping;\n }\n\n // Focus point getter/setter\n set focusPoint(point: pc.Vec3) {\n const position = this.camera.getPosition();\n this._startZoomDist = position.distance(point);\n this._controller.attach(this._pose.look(position, point), false);\n }\n\n get focusPoint(): pc.Vec3 {\n return this._pose.getFocus(tmpV1);\n }\n\n // Range getters/setters\n set pitchRange(range: pc.Vec2) {\n this._pitchRange.copy(range);\n this._flyController.pitchRange = this._pitchRange;\n this._orbitController.pitchRange = this._pitchRange;\n }\n\n get pitchRange(): pc.Vec2 {\n return this._pitchRange;\n }\n\n set yawRange(range: pc.Vec2) {\n this._yawRange.x = pc.math.clamp(range.x, -360, 360);\n this._yawRange.y = pc.math.clamp(range.y, -360, 360);\n this._flyController.yawRange = this._yawRange;\n this._orbitController.yawRange = this._yawRange;\n }\n\n get yawRange(): pc.Vec2 {\n return this._yawRange;\n }\n\n set zoomRange(range: pc.Vec2) {\n this._zoomRange.x = range.x;\n this._zoomRange.y = range.y <= range.x ? Infinity : range.y;\n this._orbitController.zoomRange = this._zoomRange;\n }\n\n get zoomRange(): pc.Vec2 {\n return this._zoomRange;\n }\n\n // Mobile input layout\n set mobileInputLayout(layout: MobileInputLayout) {\n if (!/(?:joystick|touch)-(?:joystick|touch)/.test(layout)) {\n console.warn(`CameraControls: invalid mobile input layout: ${layout}`);\n return;\n }\n this._flyMobileInput.layout = layout;\n }\n\n get mobileInputLayout(): MobileInputLayout {\n return this._flyMobileInput.layout as MobileInputLayout;\n }\n\n /**\n * Get current camera mode\n */\n get mode(): CameraMode {\n return this._mode;\n }\n\n /**\n * Set camera mode\n */\n private _setMode(mode: CameraMode): void {\n // Override mode depending on enabled features\n if (this._enableFly && !this._enableOrbit) {\n mode = 'fly';\n } else if (!this._enableFly && this._enableOrbit) {\n mode = 'orbit';\n } else if (!this._enableFly && !this._enableOrbit) {\n console.warn('CameraControls: both fly and orbit modes are disabled');\n return;\n }\n\n const previousMode = this._mode;\n if (previousMode === mode) return;\n this._mode = mode;\n\n // Detach old controller\n if (this._controller) {\n this._controller.detach();\n }\n\n // Attach new controller\n switch (this._mode) {\n case 'orbit':\n this._controller = this._orbitController;\n // When switching to orbit (especially after focus), set up orbit around the last focus point\n if (previousMode === 'focus') {\n const position = this.camera.getPosition();\n this._pose.look(position, this._lastFocusPoint);\n }\n // Reset yaw tracking to prevent false discontinuity detection on mode switch\n this._lastYaw = this._pose.angles.y;\n break;\n case 'fly':\n this._controller = this._flyController;\n break;\n case 'focus':\n this._controller = this._focusController;\n break;\n }\n this._controller.attach(this._pose, false);\n\n // Emit mode change event for UI updates\n this.app.fire('cameracontrols:modechange', this._mode);\n }\n\n /**\n * Public method to set camera mode (orbit or fly only)\n * Used by UI toggle buttons to switch between orbit and fly modes\n */\n setMode(mode: 'orbit' | 'fly'): void {\n this._setMode(mode);\n }\n\n /**\n * Focus camera on a point with smooth animation.\n * After the focus animation completes, orbit mode will use this point as the orbit center.\n */\n focus(focusPoint: pc.Vec3, resetZoom: boolean = false): void {\n // Store the focus point so orbit mode uses it as the center\n this._lastFocusPoint.copy(focusPoint);\n\n this._setMode('focus');\n const zoomDist = resetZoom\n ? this._startZoomDist\n : this.camera.getPosition().distance(focusPoint);\n this._startZoomDist = zoomDist; // Update the zoom distance for orbit mode\n const position = tmpV1.copy(this.camera.forward).mulScalar(-zoomDist).add(focusPoint);\n this._controller.attach(pose.look(position, focusPoint));\n }\n\n /**\n * Look at a point without moving\n */\n look(focusPoint: pc.Vec3, resetZoom: boolean = false): void {\n this._setMode('focus');\n const position = resetZoom\n ? tmpV1.copy(this.camera.getPosition()).sub(focusPoint).normalize().mulScalar(this._startZoomDist).add(focusPoint)\n : this.camera.getPosition();\n this._controller.attach(pose.look(position, focusPoint));\n }\n\n /**\n * Reset camera to a position looking at focus point\n */\n reset(focusPoint: pc.Vec3, position: pc.Vec3): void {\n this._setMode('focus');\n this._controller.attach(pose.look(position, focusPoint));\n }\n\n /**\n * Sync internal pose from camera's current position and rotation.\n * If a target is provided, the pose is set up to look at that target from the current position.\n * Otherwise, preserves the camera's exact rotation.\n */\n syncFromCamera(target?: pc.Vec3): void {\n const position = this.camera.getPosition().clone();\n\n if (target) {\n // When we have a target, use pose.look() to properly set up the pose\n // This calculates correct angles to look at the target from current position\n this._lastFocusPoint.copy(target);\n const focusDistance = position.distance(target);\n this._startZoomDist = focusDistance;\n this._pose.distance = focusDistance;\n\n // Use pose.look() which properly calculates yaw/pitch to face the target\n this._pose.look(position, target);\n\n // Normalize yaw to [-180, 180] range\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n\n // Clamp pitch to prevent flipping (avoid gimbal lock near ±90°)\n this._pose.angles.x = Math.max(-89, Math.min(89, this._pose.angles.x));\n\n // Ensure no roll\n this._pose.angles.z = 0;\n } else {\n // No target - preserve camera's exact rotation\n const eulerAngles = this.camera.getEulerAngles();\n\n // Set pose position directly from camera\n this._pose.position.copy(position);\n\n // Set pose angles directly from camera's euler angles\n this._pose.angles.x = eulerAngles.x;\n this._pose.angles.y = eulerAngles.y;\n this._pose.angles.z = 0; // Always zero roll to prevent flipping\n\n // Normalize yaw to [-180, 180] range\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n\n // Clamp pitch to prevent flipping\n this._pose.angles.x = Math.max(-89, Math.min(89, this._pose.angles.x));\n\n // Calculate a focus point 10 units in front of the camera\n const forward = this.camera.forward.clone();\n this._lastFocusPoint.copy(position).add(forward.mulScalar(10));\n\n const focusDistance = 10;\n this._startZoomDist = focusDistance;\n this._pose.distance = focusDistance;\n }\n\n // Reattach the current controller with the synced pose\n this._controller.attach(this._pose, false);\n }\n\n /**\n * Sync internal pose from explicit position and rotation values.\n * Use this when you need to bypass the camera entity's current state.\n * Also sets the camera entity to match these values.\n */\n syncFromPose(position: pc.Vec3, rotation: pc.Quat, focusTarget?: pc.Vec3): void {\n // Set the camera entity to match the provided pose\n this.camera.setPosition(position);\n this.camera.setRotation(rotation);\n\n // Get euler angles from the quaternion\n const tempEntity = new pc.Entity();\n tempEntity.setRotation(rotation);\n const eulerAngles = tempEntity.getEulerAngles();\n\n // Set pose position\n this._pose.position.copy(position);\n\n // Set pose angles from the rotation\n this._pose.angles.x = eulerAngles.x;\n this._pose.angles.y = eulerAngles.y;\n this._pose.angles.z = 0; // Always zero roll\n\n // Normalize yaw to [-180, 180] range\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n\n // Clamp pitch to prevent flipping\n this._pose.angles.x = Math.max(-89, Math.min(89, this._pose.angles.x));\n\n // Calculate focus point\n if (focusTarget) {\n this._lastFocusPoint.copy(focusTarget);\n } else {\n // Calculate a focus point 10 units in front\n const forward = this.camera.forward.clone();\n this._lastFocusPoint.copy(position).add(forward.mulScalar(10));\n }\n\n const focusDistance = position.distance(this._lastFocusPoint);\n this._startZoomDist = focusDistance;\n this._pose.distance = focusDistance;\n\n // Reattach the current controller with the synced pose\n this._controller.attach(this._pose, false);\n }\n\n /**\n * Enable controls\n */\n enable(): void {\n this.enabled = true;\n }\n\n /**\n * Disable controls\n */\n disable(): void {\n this.enabled = false;\n // Discard any pending inputs\n this._desktopInput.read();\n this._orbitMobileInput.read();\n this._flyMobileInput.read();\n this._gamepadInput.read();\n }\n\n /**\n * Update camera controls - call this every frame\n */\n update(dt: number): void {\n if (!this.enabled) return;\n\n const { keyCode } = pc.KeyboardMouseSource;\n const { key, button, mouse, wheel } = this._desktopInput.read();\n const { touch, pinch, count } = this._orbitMobileInput.read();\n const { leftInput, rightInput } = this._flyMobileInput.read();\n const { leftStick, rightStick } = this._gamepadInput.read();\n\n // Apply dead zone to gamepad sticks\n applyDeadZone(leftStick, this.gamepadDeadZone.x, this.gamepadDeadZone.y);\n applyDeadZone(rightStick, this.gamepadDeadZone.x, this.gamepadDeadZone.y);\n\n // Update state\n this._state.axis.add(tmpV1.set(\n (key[keyCode.D] - key[keyCode.A]) + (key[keyCode.RIGHT] - key[keyCode.LEFT]),\n (key[keyCode.E] - key[keyCode.Q]),\n (key[keyCode.W] - key[keyCode.S]) + (key[keyCode.UP] - key[keyCode.DOWN])\n ));\n for (let i = 0; i < this._state.mouse.length; i++) {\n this._state.mouse[i] += button[i];\n }\n this._state.shift += key[keyCode.SHIFT];\n this._state.ctrl += key[keyCode.CTRL];\n this._state.touches += count[0];\n\n // Mode switching based on input\n if (button[0] === 1 || button[1] === 1 || wheel[0] !== 0) {\n // Left mouse button, middle mouse button, mouse wheel -> orbit\n this._setMode('orbit');\n } else if (button[2] === 1 || this._state.axis.length() > 0) {\n // Right mouse button or any movement -> fly\n this._setMode('fly');\n }\n\n const orbit = +(this._mode === 'orbit');\n const fly = +(this._mode === 'fly');\n const double = +(this._state.touches > 1);\n const desktopPan = +(this._state.shift || this._state.mouse[1]);\n const mobileJoystick = +(this._flyMobileInput.layout.endsWith('joystick'));\n\n // Multipliers\n const moveMult = (this._state.shift ? this.moveFastSpeed : this._state.ctrl\n ? this.moveSlowSpeed : this.moveSpeed) * dt;\n const zoomMult = this.zoomSpeed * 60 * dt;\n const zoomTouchMult = zoomMult * this.zoomPinchSens;\n const rotateMult = this.rotateSpeed * 60 * dt;\n const rotateTouchMult = rotateMult * this.rotateTouchSens; // Touch-specific rotation (10x lower)\n const rotateJoystickMult = this.rotateSpeed * this.rotateJoystickSens * 60 * dt;\n\n const { deltas } = frame;\n\n // Desktop move\n const v = tmpV1.set(0, 0, 0);\n const keyMove = this._state.axis.clone().normalize();\n v.add(keyMove.mulScalar(fly * moveMult));\n const panMove = screenToWorld(this.cameraComponent, mouse[0], mouse[1], this._pose.distance);\n v.add(panMove.mulScalar(orbit * desktopPan * +this.enablePan));\n const wheelMove = tmpV2.set(0, 0, wheel[0]);\n v.add(wheelMove.mulScalar(orbit * zoomMult));\n deltas.move.append([v.x, v.y, v.z]);\n\n // Desktop rotate\n v.set(0, 0, 0);\n const mouseRotate = tmpV2.set(mouse[0], mouse[1], 0);\n v.add(mouseRotate.mulScalar((1 - (orbit * desktopPan)) * rotateMult));\n deltas.rotate.append([v.x, v.y, v.z]);\n\n // Mobile move\n v.set(0, 0, 0);\n const flyMove = tmpV2.set(leftInput[0], 0, -leftInput[1]);\n v.add(flyMove.mulScalar(fly * moveMult));\n const orbitMove = screenToWorld(this.cameraComponent, touch[0], touch[1], this._pose.distance);\n v.add(orbitMove.mulScalar(orbit * double * +this.enablePan));\n const pinchMove = tmpV2.set(0, 0, pinch[0]);\n v.add(pinchMove.mulScalar(orbit * double * zoomTouchMult));\n deltas.move.append([v.x, v.y, v.z]);\n\n // Mobile rotate (uses rotateTouchMult for reduced sensitivity on touch devices)\n v.set(0, 0, 0);\n const orbitRotate = tmpV2.set(touch[0], touch[1], 0);\n v.add(orbitRotate.mulScalar(orbit * (1 - double) * rotateTouchMult));\n const flyRotate = tmpV2.set(rightInput[0], rightInput[1], 0);\n v.add(flyRotate.mulScalar(fly * (mobileJoystick ? rotateJoystickMult : rotateTouchMult)));\n deltas.rotate.append([v.x, v.y, v.z]);\n\n // Gamepad move\n v.set(0, 0, 0);\n const stickMove = tmpV2.set(leftStick[0], 0, -leftStick[1]);\n v.add(stickMove.mulScalar(fly * moveMult));\n deltas.move.append([v.x, v.y, v.z]);\n\n // Gamepad rotate\n v.set(0, 0, 0);\n const stickRotate = tmpV2.set(rightStick[0], rightStick[1], 0);\n v.add(stickRotate.mulScalar(fly * rotateJoystickMult));\n deltas.rotate.append([v.x, v.y, v.z]);\n\n // Check if XR is active - discard frame if so\n if ((this.app as any).xr?.active) {\n frame.read();\n return;\n }\n\n // Check focus end\n if (this._mode === 'focus') {\n const focusInterrupt = deltas.move.length() + deltas.rotate.length() > 0;\n const focusComplete = (this._focusController as any).complete?.() ?? false;\n if (focusInterrupt || focusComplete) {\n this._setMode('orbit');\n }\n }\n\n // Update controller by consuming frame\n this._pose.copy(this._controller.update(frame, dt));\n\n // Fix yaw discontinuity in orbit mode\n // PlayCanvas's Pose.lerp() uses `% 360` after lerpAngle which doesn't handle\n // negative angles correctly (e.g., -185 % 360 = -185, not 175)\n // This causes sudden jumps when yaw crosses the ±180° boundary\n if (this._mode === 'orbit') {\n // Normalize yaw to [-180, 180) range to prevent accumulation issues\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n\n if (this._lastYaw !== undefined) {\n // Check for discontinuous jump (more than 90° in a single frame is unnatural)\n const yawDelta = yaw - this._lastYaw;\n\n // If there's a large jump, adjust to maintain continuity\n if (yawDelta > 180) {\n yaw -= 360;\n } else if (yawDelta < -180) {\n yaw += 360;\n }\n }\n\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n }\n\n this.camera.setPosition(this._pose.position);\n this.camera.setEulerAngles(this._pose.angles);\n }\n\n /**\n * Clean up resources\n */\n destroy(): void {\n this._desktopInput.destroy();\n this._orbitMobileInput.destroy();\n this._flyMobileInput.destroy();\n this._gamepadInput.destroy();\n\n this._flyController.destroy();\n this._orbitController.destroy();\n }\n}\n","/**\n * CharacterController - First Person Character Controller with Collisions\n *\n * A lightweight character controller for PlayCanvas that provides:\n * - WASD/Arrow key movement\n * - Mouse look\n * - Gravity and ground detection via raycasting\n * - Collision detection with scene meshes via raycasting\n *\n * Does NOT require ammo.js physics - uses simple raycasting for collisions.\n */\n\nimport * as pc from 'playcanvas';\n\nexport interface CharacterControllerConfig {\n /** Movement speed in units per second */\n moveSpeed?: number;\n /** Sprint speed multiplier */\n sprintMultiplier?: number;\n /** Mouse look sensitivity */\n lookSensitivity?: number;\n /** Player height (camera Y offset from ground) */\n playerHeight?: number;\n /** Gravity strength (units per second squared) */\n gravity?: number;\n /** Maximum fall speed */\n maxFallSpeed?: number;\n /** Jump velocity */\n jumpVelocity?: number;\n /** Collision radius for horizontal movement */\n collisionRadius?: number;\n /** Step height for climbing small obstacles */\n stepHeight?: number;\n /** Ground check distance below feet */\n groundCheckDistance?: number;\n}\n\ninterface CollisionMeshData {\n entity: pc.Entity;\n meshType: string;\n position: number[];\n rotation: number[];\n scaling: number[];\n}\n\n/**\n * CharacterController class - First person controller with collision detection\n */\nexport class CharacterController {\n private camera: pc.Entity;\n private app: pc.Application;\n private enabled: boolean = false;\n\n // Movement state\n private velocity: pc.Vec3 = new pc.Vec3();\n private isGrounded: boolean = false;\n private yaw: number = 0;\n private pitch: number = 0;\n\n // Input state\n private keys: { [key: string]: boolean } = {};\n private mouseLocked: boolean = false;\n\n // Configuration\n moveSpeed: number = 5;\n sprintMultiplier: number = 2;\n lookSensitivity: number = 0.002;\n playerHeight: number = 1.6;\n gravity: number = 20;\n maxFallSpeed: number = 50;\n jumpVelocity: number = 8;\n collisionRadius: number = 0.3;\n stepHeight: number = 0.3;\n groundCheckDistance: number = 0.1;\n\n // Collision meshes\n private collisionEntities: pc.Entity[] = [];\n private floorEntity: pc.Entity | null = null;\n\n // Event handlers (for cleanup)\n private keydownHandler: ((e: KeyboardEvent) => void) | null = null;\n private keyupHandler: ((e: KeyboardEvent) => void) | null = null;\n private mousemoveHandler: ((e: MouseEvent) => void) | null = null;\n private clickHandler: ((e: MouseEvent) => void) | null = null;\n private pointerlockchangeHandler: (() => void) | null = null;\n\n // Temporary vectors for calculations\n private tmpVec = new pc.Vec3();\n private tmpVec2 = new pc.Vec3();\n private forward = new pc.Vec3();\n private right = new pc.Vec3();\n\n constructor(camera: pc.Entity, app: pc.Application, config: CharacterControllerConfig = {}) {\n this.camera = camera;\n this.app = app;\n\n // Apply config\n if (config.moveSpeed !== undefined) this.moveSpeed = config.moveSpeed;\n if (config.sprintMultiplier !== undefined) this.sprintMultiplier = config.sprintMultiplier;\n if (config.lookSensitivity !== undefined) this.lookSensitivity = config.lookSensitivity;\n if (config.playerHeight !== undefined) this.playerHeight = config.playerHeight;\n if (config.gravity !== undefined) this.gravity = config.gravity;\n if (config.maxFallSpeed !== undefined) this.maxFallSpeed = config.maxFallSpeed;\n if (config.jumpVelocity !== undefined) this.jumpVelocity = config.jumpVelocity;\n if (config.collisionRadius !== undefined) this.collisionRadius = config.collisionRadius;\n if (config.stepHeight !== undefined) this.stepHeight = config.stepHeight;\n if (config.groundCheckDistance !== undefined) this.groundCheckDistance = config.groundCheckDistance;\n\n // Initialize yaw/pitch from camera rotation\n const angles = this.camera.getEulerAngles();\n this.pitch = angles.x;\n this.yaw = angles.y;\n }\n\n /**\n * Create collision meshes from scene data\n */\n async createCollisionMeshes(collisionMeshesData: any[]): Promise<void> {\n if (!collisionMeshesData || collisionMeshesData.length === 0) return;\n\n console.log('[CharacterController] Creating collision meshes:', collisionMeshesData.length);\n\n const loadPromises: Promise<void>[] = [];\n\n collisionMeshesData.forEach((data, index) => {\n // Handle custom mesh (GLB/GLTF files)\n if (data.meshType === 'custom' && data.customMeshUrl) {\n const promise = this.loadCustomCollisionMesh(data, index);\n loadPromises.push(promise);\n return;\n }\n\n let entity: pc.Entity | null = null;\n\n switch (data.meshType) {\n case 'cube':\n entity = new pc.Entity(`collision-cube-${index}`);\n entity.addComponent('render', {\n type: 'box'\n });\n break;\n case 'sphere':\n entity = new pc.Entity(`collision-sphere-${index}`);\n entity.addComponent('render', {\n type: 'sphere'\n });\n break;\n case 'floor':\n entity = new pc.Entity(`collision-floor-${index}`);\n entity.addComponent('render', {\n type: 'plane'\n });\n this.floorEntity = entity;\n break;\n case 'plane':\n default:\n entity = new pc.Entity(`collision-plane-${index}`);\n entity.addComponent('render', {\n type: 'plane'\n });\n break;\n }\n\n if (entity) {\n this.configureCollisionEntity(entity, data);\n }\n });\n\n // Wait for all custom meshes to load\n if (loadPromises.length > 0) {\n await Promise.all(loadPromises);\n }\n\n console.log('[CharacterController] Created', this.collisionEntities.length, 'collision entities');\n }\n\n /**\n * Load a custom collision mesh (GLB/GLTF)\n */\n private async loadCustomCollisionMesh(data: any, index: number): Promise<void> {\n const url = data.customMeshUrl;\n console.log('[CharacterController] Loading custom collision mesh:', url);\n\n try {\n // Determine asset type from URL\n const ext = url.split('?')[0].split('.').pop()?.toLowerCase() || 'glb';\n const assetType = (ext === 'gltf' || ext === 'glb') ? 'container' : 'model';\n\n const asset = new pc.Asset(`collision-custom-${index}`, assetType, { url });\n\n await new Promise<void>((resolve, reject) => {\n asset.ready(() => {\n try {\n const entity = new pc.Entity(`collision-custom-${index}`);\n\n if (assetType === 'container') {\n // GLB/GLTF container - instantiate the model\n const containerResource = asset.resource as pc.ContainerResource;\n if (containerResource && containerResource.instantiateRenderEntity) {\n const renderEntity = containerResource.instantiateRenderEntity();\n // Re-parent all children to our collision entity\n while (renderEntity.children.length > 0) {\n entity.addChild(renderEntity.children[0]);\n }\n renderEntity.destroy();\n }\n } else {\n // Regular model\n entity.addComponent('model', {\n asset: asset\n });\n }\n\n this.configureCollisionEntity(entity, data);\n\n // Store bounding box for custom mesh collision\n this.computeAndStoreBounds(entity);\n\n resolve();\n } catch (err) {\n console.error('[CharacterController] Error setting up custom mesh:', err);\n reject(err);\n }\n });\n\n asset.on('error', (err: any) => {\n console.error('[CharacterController] Error loading custom mesh:', url, err);\n reject(err);\n });\n\n this.app.assets.add(asset);\n this.app.assets.load(asset);\n });\n } catch (error) {\n console.error('[CharacterController] Failed to load custom collision mesh:', url, error);\n }\n }\n\n /**\n * Compute and store bounding box for a collision entity\n */\n private computeAndStoreBounds(entity: pc.Entity): void {\n // Traverse entity and its children to compute combined bounds\n const bounds = new pc.BoundingBox();\n let boundsInitialized = false;\n\n const traverse = (node: pc.Entity) => {\n if (node.render && node.render.meshInstances) {\n for (const mi of node.render.meshInstances) {\n if (mi.aabb) {\n if (!boundsInitialized) {\n bounds.copy(mi.aabb);\n boundsInitialized = true;\n } else {\n bounds.add(mi.aabb);\n }\n }\n }\n }\n for (const child of node.children) {\n if (child instanceof pc.Entity) {\n traverse(child);\n }\n }\n };\n\n traverse(entity);\n\n if (boundsInitialized) {\n (entity as any)._collisionBounds = bounds;\n console.log('[CharacterController] Computed bounds for custom mesh:', bounds.halfExtents);\n }\n }\n\n /**\n * Configure a collision entity with transform and visibility\n */\n private configureCollisionEntity(entity: pc.Entity, data: any): void {\n // Apply transform - convert from BabylonJS coordinates\n const pos = data.position || [0, 0, 0];\n entity.setPosition(pos[0], pos[1], -pos[2]); // Negate Z\n\n const rot = data.rotation || [0, 0, 0];\n entity.setEulerAngles(\n rot[0] * (180 / Math.PI),\n rot[1] * (180 / Math.PI),\n -rot[2] * (180 / Math.PI)\n );\n\n const scale = data.scaling || [1, 1, 1];\n entity.setLocalScale(scale[0], scale[1], scale[2]);\n\n // Make invisible but keep for collision\n this.setEntityVisibility(entity, false);\n\n // Store mesh type for collision logic\n (entity as any)._collisionMeshType = data.meshType;\n\n this.app.root.addChild(entity);\n this.collisionEntities.push(entity);\n }\n\n /**\n * Set visibility of entity and all children\n */\n private setEntityVisibility(entity: pc.Entity, visible: boolean): void {\n if (entity.render) {\n entity.render.enabled = visible;\n }\n for (const child of entity.children) {\n if (child instanceof pc.Entity) {\n this.setEntityVisibility(child, visible);\n }\n }\n }\n\n /**\n * Enable the character controller\n */\n enable(): void {\n if (this.enabled) return;\n this.enabled = true;\n\n // Sync yaw/pitch from current camera rotation\n const angles = this.camera.getEulerAngles();\n this.pitch = angles.x;\n this.yaw = angles.y;\n\n // Reset velocity\n this.velocity.set(0, 0, 0);\n\n // Setup input handlers\n this.setupInputHandlers();\n\n console.log('[CharacterController] Enabled');\n }\n\n /**\n * Disable the character controller\n */\n disable(): void {\n if (!this.enabled) return;\n this.enabled = false;\n\n // Remove input handlers\n this.removeInputHandlers();\n\n // Exit pointer lock\n if (document.pointerLockElement) {\n document.exitPointerLock();\n }\n\n console.log('[CharacterController] Disabled');\n }\n\n /**\n * Setup keyboard and mouse input handlers\n */\n private setupInputHandlers(): void {\n const canvas = this.app.graphicsDevice.canvas as HTMLCanvasElement;\n\n // Keyboard handlers\n this.keydownHandler = (e: KeyboardEvent) => {\n this.keys[e.code] = true;\n // Jump on space\n if (e.code === 'Space' && this.isGrounded) {\n this.velocity.y = this.jumpVelocity;\n this.isGrounded = false;\n }\n };\n\n this.keyupHandler = (e: KeyboardEvent) => {\n this.keys[e.code] = false;\n };\n\n // Mouse look handler\n this.mousemoveHandler = (e: MouseEvent) => {\n if (!this.mouseLocked) return;\n\n this.yaw -= e.movementX * this.lookSensitivity * 100;\n this.pitch -= e.movementY * this.lookSensitivity * 100;\n\n // Clamp pitch\n this.pitch = Math.max(-89, Math.min(89, this.pitch));\n };\n\n // Click to lock pointer\n this.clickHandler = () => {\n if (!this.mouseLocked) {\n canvas.requestPointerLock();\n }\n };\n\n // Pointer lock change handler\n this.pointerlockchangeHandler = () => {\n this.mouseLocked = document.pointerLockElement === canvas;\n };\n\n // Add event listeners\n document.addEventListener('keydown', this.keydownHandler);\n document.addEventListener('keyup', this.keyupHandler);\n document.addEventListener('mousemove', this.mousemoveHandler);\n canvas.addEventListener('click', this.clickHandler);\n document.addEventListener('pointerlockchange', this.pointerlockchangeHandler);\n }\n\n /**\n * Remove input handlers\n */\n private removeInputHandlers(): void {\n const canvas = this.app.graphicsDevice.canvas as HTMLCanvasElement;\n\n if (this.keydownHandler) {\n document.removeEventListener('keydown', this.keydownHandler);\n }\n if (this.keyupHandler) {\n document.removeEventListener('keyup', this.keyupHandler);\n }\n if (this.mousemoveHandler) {\n document.removeEventListener('mousemove', this.mousemoveHandler);\n }\n if (this.clickHandler) {\n canvas.removeEventListener('click', this.clickHandler);\n }\n if (this.pointerlockchangeHandler) {\n document.removeEventListener('pointerlockchange', this.pointerlockchangeHandler);\n }\n\n this.keys = {};\n }\n\n /**\n * Check if position collides with any collision mesh\n */\n private checkCollision(position: pc.Vec3, radius: number): boolean {\n // AABB collision check against collision entities\n for (const entity of this.collisionEntities) {\n const entityPos = entity.getPosition();\n const entityScale = entity.getLocalScale();\n const meshType = (entity as any)._collisionMeshType;\n\n if (meshType === 'floor' || meshType === 'plane') {\n // Skip floor for horizontal collision\n continue;\n }\n\n let halfWidth: number, halfHeight: number, halfDepth: number;\n\n // Check if entity has custom bounds (for custom meshes)\n const customBounds = (entity as any)._collisionBounds as pc.BoundingBox | undefined;\n if (customBounds) {\n // Use the actual mesh bounds, scaled by entity scale\n halfWidth = customBounds.halfExtents.x * entityScale.x;\n halfHeight = customBounds.halfExtents.y * entityScale.y;\n halfDepth = customBounds.halfExtents.z * entityScale.z;\n } else {\n // Use entity scale for primitive shapes\n halfWidth = entityScale.x / 2;\n halfHeight = entityScale.y / 2;\n halfDepth = entityScale.z / 2;\n }\n\n const dx = Math.abs(position.x - entityPos.x);\n const dy = Math.abs(position.y - entityPos.y);\n const dz = Math.abs(position.z - entityPos.z);\n\n if (dx < halfWidth + radius &&\n dy < halfHeight + this.playerHeight / 2 &&\n dz < halfDepth + radius) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Check ground below player\n */\n private checkGround(position: pc.Vec3): number | null {\n // Check against floor entity if exists\n if (this.floorEntity) {\n const floorPos = this.floorEntity.getPosition();\n const floorScale = this.floorEntity.getLocalScale();\n\n // Check if we're above the floor plane\n const halfWidth = floorScale.x / 2 * 100; // Floors are typically large\n const halfDepth = floorScale.z / 2 * 100;\n\n if (Math.abs(position.x - floorPos.x) < halfWidth &&\n Math.abs(position.z - floorPos.z) < halfDepth) {\n return floorPos.y;\n }\n }\n\n // Check against other collision meshes for ground\n let highestGround: number | null = null;\n\n for (const entity of this.collisionEntities) {\n const entityPos = entity.getPosition();\n const entityScale = entity.getLocalScale();\n const meshType = (entity as any)._collisionMeshType;\n\n // Skip floor/plane (handled above) and sphere (not good for walking on)\n if (meshType === 'floor' || meshType === 'plane' || meshType === 'sphere') {\n continue;\n }\n\n let halfWidth: number, halfHeight: number, halfDepth: number;\n\n // Check if entity has custom bounds (for custom meshes)\n const customBounds = (entity as any)._collisionBounds as pc.BoundingBox | undefined;\n if (customBounds) {\n halfWidth = customBounds.halfExtents.x * entityScale.x;\n halfHeight = customBounds.halfExtents.y * entityScale.y;\n halfDepth = customBounds.halfExtents.z * entityScale.z;\n } else {\n halfWidth = entityScale.x / 2;\n halfHeight = entityScale.y / 2;\n halfDepth = entityScale.z / 2;\n }\n\n const topY = entityPos.y + halfHeight;\n\n // Check if we're above this mesh\n if (Math.abs(position.x - entityPos.x) < halfWidth + this.collisionRadius &&\n Math.abs(position.z - entityPos.z) < halfDepth + this.collisionRadius &&\n position.y >= topY - this.stepHeight) {\n if (highestGround === null || topY > highestGround) {\n highestGround = topY;\n }\n }\n }\n\n return highestGround;\n }\n\n /**\n * Update the character controller - call this every frame\n */\n update(dt: number): void {\n if (!this.enabled) return;\n\n // Get movement input\n const moveX = (this.keys['KeyD'] || this.keys['ArrowRight'] ? 1 : 0) -\n (this.keys['KeyA'] || this.keys['ArrowLeft'] ? 1 : 0);\n const moveZ = (this.keys['KeyW'] || this.keys['ArrowUp'] ? 1 : 0) -\n (this.keys['KeyS'] || this.keys['ArrowDown'] ? 1 : 0);\n const isSprinting = this.keys['ShiftLeft'] || this.keys['ShiftRight'];\n\n // Calculate forward and right vectors (horizontal only)\n const yawRad = this.yaw * (Math.PI / 180);\n this.forward.set(-Math.sin(yawRad), 0, -Math.cos(yawRad));\n this.right.set(Math.cos(yawRad), 0, -Math.sin(yawRad));\n\n // Calculate desired movement\n const speed = this.moveSpeed * (isSprinting ? this.sprintMultiplier : 1);\n this.tmpVec.set(0, 0, 0);\n this.tmpVec.add(this.tmpVec2.copy(this.forward).mulScalar(moveZ * speed));\n this.tmpVec.add(this.tmpVec2.copy(this.right).mulScalar(moveX * speed));\n\n // Apply gravity\n if (!this.isGrounded) {\n this.velocity.y -= this.gravity * dt;\n this.velocity.y = Math.max(-this.maxFallSpeed, this.velocity.y);\n }\n\n // Get current position\n const currentPos = this.camera.getPosition().clone();\n const feetY = currentPos.y - this.playerHeight;\n\n // Calculate new position\n const newPos = new pc.Vec3();\n newPos.x = currentPos.x + this.tmpVec.x * dt;\n newPos.y = currentPos.y + this.velocity.y * dt;\n newPos.z = currentPos.z + this.tmpVec.z * dt;\n\n // Check horizontal collision (X axis)\n this.tmpVec2.set(newPos.x, currentPos.y, currentPos.z);\n if (this.checkCollision(this.tmpVec2, this.collisionRadius)) {\n newPos.x = currentPos.x;\n }\n\n // Check horizontal collision (Z axis)\n this.tmpVec2.set(newPos.x, currentPos.y, newPos.z);\n if (this.checkCollision(this.tmpVec2, this.collisionRadius)) {\n newPos.z = currentPos.z;\n }\n\n // Check ground\n const groundY = this.checkGround(newPos);\n const targetFeetY = newPos.y - this.playerHeight;\n\n if (groundY !== null && targetFeetY <= groundY + this.groundCheckDistance) {\n // On ground\n newPos.y = groundY + this.playerHeight;\n this.velocity.y = 0;\n this.isGrounded = true;\n } else if (groundY === null || targetFeetY > groundY + this.stepHeight) {\n // In air\n this.isGrounded = false;\n }\n\n // Apply new position\n this.camera.setPosition(newPos.x, newPos.y, newPos.z);\n\n // Apply rotation\n this.camera.setEulerAngles(this.pitch, this.yaw, 0);\n }\n\n /**\n * Clean up resources\n */\n destroy(): void {\n this.disable();\n\n // Remove collision entities\n for (const entity of this.collisionEntities) {\n entity.destroy();\n }\n this.collisionEntities = [];\n this.floorEntity = null;\n }\n\n /**\n * Get whether the controller is grounded\n */\n get grounded(): boolean {\n return this.isGrounded;\n }\n\n /**\n * Get current velocity\n */\n getVelocity(): pc.Vec3 {\n return this.velocity.clone();\n }\n\n /**\n * Set position\n */\n setPosition(x: number, y: number, z: number): void {\n this.camera.setPosition(x, y, z);\n }\n\n /**\n * Set rotation (yaw and pitch in degrees)\n */\n setRotation(pitch: number, yaw: number): void {\n this.pitch = pitch;\n this.yaw = yaw;\n this.camera.setEulerAngles(pitch, yaw, 0);\n }\n}\n","/**\n * GsplatRevealRadial - Radial reveal effect for gaussian splats\n *\n * Creates two waves emanating from a center point:\n * 1. Dot wave: Small colored dots appear progressively\n * 2. Lift wave: Particles lift up, get highlighted, then settle to original state\n *\n * Ported from PlayCanvas engine examples\n */\n\nimport * as pc from 'playcanvas';\n\nconst shaderGLSL = /* glsl */ `\nuniform float uTime;\nuniform vec3 uCenter;\nuniform float uSpeed;\nuniform float uAcceleration;\nuniform float uDelay;\nuniform vec3 uDotTint;\nuniform vec3 uWaveTint;\nuniform float uOscillationIntensity;\nuniform float uEndRadius;\n\n// Shared globals (initialized once per vertex)\nfloat g_dist;\nfloat g_dotWavePos;\nfloat g_liftTime;\nfloat g_liftWavePos;\n\nvoid initShared(vec3 center) {\n g_dist = length(center - uCenter);\n g_dotWavePos = uSpeed * uTime + 0.5 * uAcceleration * uTime * uTime;\n g_liftTime = max(0.0, uTime - uDelay);\n g_liftWavePos = uSpeed * g_liftTime + 0.5 * uAcceleration * g_liftTime * g_liftTime;\n}\n\n// Hash function for per-splat randomization\nfloat hash(vec3 p) {\n return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);\n}\n\nvoid modifyCenter(inout vec3 center) {\n initShared(center);\n\n // Early exit optimization\n if (g_dist > uEndRadius) return;\n\n // Only apply oscillation if lift wave hasn't fully passed\n bool wavesActive = g_liftTime <= 0.0 || g_dist > g_liftWavePos - 1.5;\n if (wavesActive) {\n // Apply oscillation with per-splat phase offset\n float phase = hash(center) * 6.28318;\n center.y += sin(uTime * 3.0 + phase) * uOscillationIntensity * 0.25;\n }\n\n // Apply lift effect near the wave edge\n float distToLiftWave = abs(g_dist - g_liftWavePos);\n if (distToLiftWave < 1.0 && g_liftTime > 0.0) {\n // Create a smooth lift curve (peaks at wave edge)\n // Lift is 0.9x the oscillation intensity (30% of original 3x)\n float liftAmount = (1.0 - distToLiftWave) * sin(distToLiftWave * 3.14159);\n center.y += liftAmount * uOscillationIntensity * 0.9;\n }\n}\n\nvoid modifyCovariance(vec3 originalCenter, vec3 modifiedCenter, inout vec3 covA, inout vec3 covB) {\n // Early exit for distant splats - hide them\n if (g_dist > uEndRadius) {\n gsplatMakeRound(covA, covB, 0.0);\n return;\n }\n\n // Determine scale and phase\n float scale;\n bool isLiftWave = g_liftTime > 0.0 && g_liftWavePos > g_dist;\n\n if (isLiftWave) {\n // Lift wave: transition from dots to full size\n scale = (g_liftWavePos >= g_dist + 2.0) ? 1.0 : mix(0.1, 1.0, (g_liftWavePos - g_dist) * 0.5);\n } else if (g_dist > g_dotWavePos + 1.0) {\n // Before dot wave: invisible\n gsplatMakeRound(covA, covB, 0.0);\n return;\n } else if (g_dist > g_dotWavePos - 1.0) {\n // Dot wave front: scale from 0 to 0.1 with 2x peak at center\n float distToWave = abs(g_dist - g_dotWavePos);\n scale = (distToWave < 0.5)\n ? mix(0.1, 0.2, 1.0 - distToWave * 2.0)\n : mix(0.0, 0.1, smoothstep(g_dotWavePos + 1.0, g_dotWavePos - 1.0, g_dist));\n } else {\n // After dot wave, before lift: small dots\n scale = 0.1;\n }\n\n // Apply scale to covariance\n if (scale >= 1.0) {\n // Fully revealed: original shape and size (no-op)\n return;\n } else if (isLiftWave) {\n // Lift wave: lerp from round dots to original shape\n float t = (scale - 0.1) * 1.111111; // normalize [0.1, 1.0] to [0, 1]\n float dotSize = scale * 0.05;\n float originalSize = gsplatExtractSize(covA, covB);\n float finalSize = mix(dotSize, originalSize, t);\n\n // Lerp between round and scaled original\n vec3 origCovA = covA * (scale * scale);\n vec3 origCovB = covB * (scale * scale);\n gsplatMakeRound(covA, covB, finalSize);\n covA = mix(covA, origCovA, t);\n covB = mix(covB, origCovB, t);\n } else {\n // Dot phase: round with absolute size, but don't make small splats larger\n float originalSize = gsplatExtractSize(covA, covB);\n gsplatMakeRound(covA, covB, min(scale * 0.05, originalSize));\n }\n}\n\nvoid modifyColor(vec3 center, inout vec4 color) {\n // Use shared globals\n if (g_dist > uEndRadius) return;\n\n // Lift wave tint takes priority (active during lift)\n if (g_liftTime > 0.0 && g_dist >= g_liftWavePos - 1.5 && g_dist <= g_liftWavePos + 0.5) {\n float distToLift = abs(g_dist - g_liftWavePos);\n float liftIntensity = smoothstep(1.5, 0.0, distToLift);\n color.rgb += uWaveTint * liftIntensity;\n }\n // Dot wave tint (active in dot phase, but not where lift wave is active)\n else if (g_dist <= g_dotWavePos && (g_liftTime <= 0.0 || g_dist > g_liftWavePos + 0.5)) {\n float distToDot = abs(g_dist - g_dotWavePos);\n float dotIntensity = smoothstep(1.0, 0.0, distToDot);\n color.rgb += uDotTint * dotIntensity;\n }\n}\n`;\n\nconst shaderWGSL = /* wgsl */ `\nuniform uTime: f32;\nuniform uCenter: vec3f;\nuniform uSpeed: f32;\nuniform uAcceleration: f32;\nuniform uDelay: f32;\nuniform uDotTint: vec3f;\nuniform uWaveTint: vec3f;\nuniform uOscillationIntensity: f32;\nuniform uEndRadius: f32;\n\n// Shared globals (initialized once per vertex)\nvar<private> g_dist: f32;\nvar<private> g_dotWavePos: f32;\nvar<private> g_liftTime: f32;\nvar<private> g_liftWavePos: f32;\n\nfn initShared(center: vec3f) {\n g_dist = length(center - uniform.uCenter);\n g_dotWavePos = uniform.uSpeed * uniform.uTime + 0.5 * uniform.uAcceleration * uniform.uTime * uniform.uTime;\n g_liftTime = max(0.0, uniform.uTime - uniform.uDelay);\n g_liftWavePos = uniform.uSpeed * g_liftTime + 0.5 * uniform.uAcceleration * g_liftTime * g_liftTime;\n}\n\n// Hash function for per-splat randomization\nfn hash(p: vec3f) -> f32 {\n return fract(sin(dot(p, vec3f(127.1, 311.7, 74.7))) * 43758.5453);\n}\n\nfn modifyCenter(center: ptr<function, vec3f>) {\n initShared(*center);\n\n // Early exit optimization\n if (g_dist > uniform.uEndRadius) {\n return;\n }\n\n // Only apply oscillation if lift wave hasn't fully passed\n let wavesActive = g_liftTime <= 0.0 || g_dist > g_liftWavePos - 1.5;\n if (wavesActive) {\n // Apply oscillation with per-splat phase offset\n let phase = hash(*center) * 6.28318;\n (*center).y += sin(uniform.uTime * 3.0 + phase) * uniform.uOscillationIntensity * 0.25;\n }\n\n // Apply lift effect near the wave edge\n let distToLiftWave = abs(g_dist - g_liftWavePos);\n if (distToLiftWave < 1.0 && g_liftTime > 0.0) {\n // Create a smooth lift curve (peaks at wave edge)\n // Lift is 0.9x the oscillation intensity (30% of original 3x)\n let liftAmount = (1.0 - distToLiftWave) * sin(distToLiftWave * 3.14159);\n (*center).y += liftAmount * uniform.uOscillationIntensity * 0.9;\n }\n}\n\nfn modifyCovariance(originalCenter: vec3f, modifiedCenter: vec3f, covA: ptr<function, vec3f>, covB: ptr<function, vec3f>) {\n // Early exit for distant splats - hide them\n if (g_dist > uniform.uEndRadius) {\n gsplatMakeRound(covA, covB, 0.0);\n return;\n }\n\n // Determine scale and phase\n var scale: f32;\n let isLiftWave = g_liftTime > 0.0 && g_liftWavePos > g_dist;\n\n if (isLiftWave) {\n // Lift wave: transition from dots to full size\n scale = select(mix(0.1, 1.0, (g_liftWavePos - g_dist) * 0.5), 1.0, g_liftWavePos >= g_dist + 2.0);\n } else if (g_dist > g_dotWavePos + 1.0) {\n // Before dot wave: invisible\n gsplatMakeRound(covA, covB, 0.0);\n return;\n } else if (g_dist > g_dotWavePos - 1.0) {\n // Dot wave front: scale from 0 to 0.1 with 2x peak at center\n let distToWave = abs(g_dist - g_dotWavePos);\n scale = select(\n mix(0.0, 0.1, smoothstep(g_dotWavePos + 1.0, g_dotWavePos - 1.0, g_dist)),\n mix(0.1, 0.2, 1.0 - distToWave * 2.0),\n distToWave < 0.5\n );\n } else {\n // After dot wave, before lift: small dots\n scale = 0.1;\n }\n\n // Apply scale to covariance\n if (scale >= 1.0) {\n // Fully revealed: original shape and size (no-op)\n return;\n } else if (isLiftWave) {\n // Lift wave: lerp from round dots to original shape\n let t = (scale - 0.1) * 1.111111; // normalize [0.1, 1.0] to [0, 1]\n let dotSize = scale * 0.05;\n let originalSize = gsplatExtractSize(*covA, *covB);\n let finalSize = mix(dotSize, originalSize, t);\n\n // Lerp between round and scaled original\n let origCovA = *covA * (scale * scale);\n let origCovB = *covB * (scale * scale);\n gsplatMakeRound(covA, covB, finalSize);\n *covA = mix(*covA, origCovA, t);\n *covB = mix(*covB, origCovB, t);\n } else {\n // Dot phase: round with absolute size, but don't make small splats larger\n let originalSize = gsplatExtractSize(*covA, *covB);\n gsplatMakeRound(covA, covB, min(scale * 0.05, originalSize));\n }\n}\n\nfn modifyColor(center: vec3f, color: ptr<function, vec4f>) {\n // Use shared globals\n if (g_dist > uniform.uEndRadius) {\n return;\n }\n\n // Lift wave tint takes priority (active during lift)\n if (g_liftTime > 0.0 && g_dist >= g_liftWavePos - 1.5 && g_dist <= g_liftWavePos + 0.5) {\n let distToLift = abs(g_dist - g_liftWavePos);\n let liftIntensity = smoothstep(1.5, 0.0, distToLift);\n (*color) = vec4f((*color).rgb + uniform.uWaveTint * liftIntensity, (*color).a);\n }\n // Dot wave tint (active in dot phase, but not where lift wave is active)\n else if (g_dist <= g_dotWavePos && (g_liftTime <= 0.0 || g_dist > g_liftWavePos + 0.5)) {\n let distToDot = abs(g_dist - g_dotWavePos);\n let dotIntensity = smoothstep(1.0, 0.0, distToDot);\n (*color) = vec4f((*color).rgb + uniform.uDotTint * dotIntensity, (*color).a);\n }\n}\n`;\n\n// Cache for the script class\nlet _GsplatRevealRadialClass: typeof pc.ScriptType | null = null;\n\n/**\n * Gets or creates the GsplatRevealRadial script class.\n * Must be called after PlayCanvas app is initialized.\n */\nexport function getGsplatRevealRadialClass(): typeof pc.ScriptType {\n console.log('[RevealEffect] getGsplatRevealRadialClass called, cached:', !!_GsplatRevealRadialClass);\n\n if (_GsplatRevealRadialClass) {\n return _GsplatRevealRadialClass;\n }\n\n console.log('[RevealEffect] Creating new script class via pc.createScript');\n const GsplatRevealRadial = pc.createScript('gsplatRevealRadial') as any;\n console.log('[RevealEffect] Script class created:', GsplatRevealRadial);\n\n Object.assign(GsplatRevealRadial.prototype, {\n // Properties\n effectTime: 0,\n _materialsApplied: null as Set<pc.Material> | null,\n _shadersApplied: false,\n _retryCount: 0,\n _maxRetries: 100,\n _materialCreatedHandler: null as ((material: pc.Material) => void) | null,\n _systemMaterialHandler: null as ((material: pc.Material, camera: any, layer: any) => void) | null,\n\n // Reusable arrays for uniform updates\n _centerArray: [0, 0, 0],\n _dotTintArray: [0, 0, 0],\n _waveTintArray: [0, 0, 0],\n\n // Effect properties\n center: null as pc.Vec3 | null,\n speed: 5,\n acceleration: 0,\n delay: 2,\n dotTint: null as pc.Color | null,\n waveTint: null as pc.Color | null,\n oscillationIntensity: 0.2,\n endRadius: 25,\n\n initialize(this: any) {\n console.log('[RevealEffect] initialize() called');\n this.effectTime = 0;\n this._materialsApplied = new Set();\n this._shadersApplied = false;\n this._retryCount = 0;\n this._centerArray = [0, 0, 0];\n this._dotTintArray = [0, 0, 0];\n this._waveTintArray = [0, 0, 0];\n\n // Initialize Vec3 and Color if not set\n if (!this.center) {\n this.center = new pc.Vec3(0, 0, 0);\n }\n if (!this.dotTint) {\n this.dotTint = new pc.Color(0, 1, 1); // Cyan\n }\n if (!this.waveTint) {\n this.waveTint = new pc.Color(1, 0.5, 0); // Orange\n }\n\n this.on('enable', () => {\n console.log('[RevealEffect] enabled event fired');\n this.effectTime = 0;\n this._applyShaders();\n });\n\n this.on('disable', () => {\n console.log('[RevealEffect] disabled event fired');\n this._removeShaders();\n });\n\n if (this.enabled) {\n console.log('[RevealEffect] Starting enabled, applying shaders');\n this._applyShaders();\n } else {\n console.log('[RevealEffect] Starting disabled, waiting for enable');\n }\n },\n\n update(this: any, dt: number) {\n if (!this._shadersApplied && this._retryCount < this._maxRetries) {\n this._retryCount++;\n if (this._retryCount % 20 === 0) {\n console.log(`[RevealEffect] Retry ${this._retryCount}/${this._maxRetries} to apply shaders`);\n }\n this._applyShaders();\n }\n\n this.effectTime += dt;\n\n // Log every second\n if (Math.floor(this.effectTime) !== Math.floor(this.effectTime - dt)) {\n console.log(`[RevealEffect] effectTime: ${this.effectTime.toFixed(2)}s, shadersApplied: ${this._shadersApplied}, materialsCount: ${this._materialsApplied?.size || 0}`);\n }\n\n if (this._isEffectComplete()) {\n console.log('[RevealEffect] Effect complete, disabling');\n this.enabled = false;\n return;\n }\n\n this._updateUniforms();\n },\n\n _updateUniforms(this: any) {\n this._setUniform('uTime', this.effectTime);\n\n this._centerArray[0] = this.center.x;\n this._centerArray[1] = this.center.y;\n this._centerArray[2] = this.center.z;\n this._setUniform('uCenter', this._centerArray);\n\n this._setUniform('uSpeed', this.speed);\n this._setUniform('uAcceleration', this.acceleration);\n this._setUniform('uDelay', this.delay);\n\n this._dotTintArray[0] = this.dotTint.r;\n this._dotTintArray[1] = this.dotTint.g;\n this._dotTintArray[2] = this.dotTint.b;\n this._setUniform('uDotTint', this._dotTintArray);\n\n this._waveTintArray[0] = this.waveTint.r;\n this._waveTintArray[1] = this.waveTint.g;\n this._waveTintArray[2] = this.waveTint.b;\n this._setUniform('uWaveTint', this._waveTintArray);\n\n this._setUniform('uOscillationIntensity', this.oscillationIntensity);\n this._setUniform('uEndRadius', this.endRadius);\n },\n\n _getCompletionTime(this: any): number {\n const liftStartTime = this.delay;\n\n if (this.acceleration === 0) {\n return liftStartTime + (this.endRadius / this.speed);\n }\n\n const discriminant = this.speed * this.speed + 2 * this.acceleration * this.endRadius;\n if (discriminant < 0) {\n return Infinity;\n }\n const t = (-this.speed + Math.sqrt(discriminant)) / this.acceleration;\n return liftStartTime + t;\n },\n\n _isEffectComplete(this: any): boolean {\n return this.effectTime >= this._getCompletionTime();\n },\n\n getShaderGLSL(): string {\n return shaderGLSL;\n },\n\n getShaderWGSL(): string {\n return shaderWGSL;\n },\n\n // Shader application methods\n _applyShaders(this: any) {\n const gsplatComponent = this.entity.gsplat;\n if (!gsplatComponent) {\n console.log('[RevealEffect] _applyShaders: No gsplat component found on entity');\n return;\n }\n\n // Check if unified mode is enabled\n const isUnified = (gsplatComponent as any).unified === true;\n const app = this.app;\n\n if (isUnified) {\n console.log('[RevealEffect] Unified mode detected, using GSplatComponentSystem');\n\n // In unified mode, materials are accessed via GSplatComponentSystem\n const gsplatSystem = app?.systems?.gsplat;\n if (gsplatSystem) {\n // Subscribe to material:created event if not already subscribed\n if (!this._systemMaterialHandler) {\n this._systemMaterialHandler = (material: pc.Material, camera: any, layer: any) => {\n console.log('[RevealEffect] material:created event from GSplatComponentSystem');\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n };\n gsplatSystem.on('material:created', this._systemMaterialHandler);\n console.log('[RevealEffect] Subscribed to GSplatComponentSystem material:created event');\n }\n\n // Try to get existing materials for all camera/layer combinations\n const cameras = app.root?.findComponents('camera') || [];\n const layers = gsplatComponent.layers || [0]; // Default to layer 0 if not specified\n\n for (const cameraComp of cameras) {\n for (const layerId of layers) {\n const layer = app.scene?.layers?.getLayerById(layerId);\n if (layer) {\n const material = gsplatSystem.getGSplatMaterial?.(cameraComp.camera, layer);\n if (material) {\n console.log('[RevealEffect] Found unified material via getGSplatMaterial:', material);\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n }\n }\n }\n }\n }\n return;\n }\n\n // Non-unified mode: Check both instance and _instance\n const instance = (gsplatComponent as any).instance || (gsplatComponent as any)._instance;\n\n if (instance) {\n console.log('[RevealEffect] Instance found via component:', instance);\n this._applyToInstance(instance);\n return;\n }\n\n // Try to find gsplat materials through the scene's mesh instances\n if (app && app.scene) {\n // Search through layers for gsplat mesh instances\n const layers = app.scene.layers?.layerList || [];\n for (const layer of layers) {\n if (!layer.meshInstances) continue;\n\n for (const mi of layer.meshInstances) {\n // Check if this is a gsplat mesh instance\n if (mi.gsplatInstance || mi._gsplatInstance) {\n const gsplatInst = mi.gsplatInstance || mi._gsplatInstance;\n console.log('[RevealEffect] Found gsplat instance via mesh instance:', gsplatInst);\n this._applyToInstance(gsplatInst);\n return;\n }\n\n // Check material for gsplat characteristics\n const material = mi.material;\n if (material && (material as any).gsplat) {\n console.log('[RevealEffect] Found gsplat material via mesh instance:', material);\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n return;\n }\n }\n }\n\n // Also check root entities for gsplat components\n const checkEntity = (entity: any) => {\n if (entity.gsplat) {\n const inst = entity.gsplat.instance || entity.gsplat._instance;\n if (inst) {\n console.log('[RevealEffect] Found gsplat instance via entity search:', inst);\n this._applyToInstance(inst);\n return true;\n }\n }\n for (const child of entity.children || []) {\n if (checkEntity(child)) return true;\n }\n return false;\n };\n\n if (app.root) {\n checkEntity(app.root);\n }\n }\n\n // Log state for debugging\n if (this._retryCount % 50 === 0) {\n console.log('[RevealEffect] Still searching for gsplat materials...');\n console.log('[RevealEffect] gsplatComponent.unified:', (gsplatComponent as any).unified);\n console.log('[RevealEffect] gsplatComponent.asset:', gsplatComponent.asset);\n }\n },\n\n _applyToInstance(this: any, instance: any) {\n if (this._shadersApplied) {\n return;\n }\n\n console.log('[RevealEffect] Applying shaders to instance');\n console.log('[RevealEffect] Instance type:', instance.constructor?.name);\n console.log('[RevealEffect] Instance keys:', Object.keys(instance));\n\n // Try to find materials on the instance\n const materials = instance.materials || instance._materials;\n console.log('[RevealEffect] Instance materials:', materials);\n\n if (materials) {\n if (materials instanceof Map || (materials.forEach && materials.size !== undefined)) {\n console.log('[RevealEffect] Materials is a Map/Set with size:', materials.size);\n if (materials.size > 0) {\n materials.forEach((material: pc.Material) => {\n this._applyShaderToMaterial(material);\n });\n this._shadersApplied = true;\n console.log('[RevealEffect] SUCCESS: Shaders applied to', materials.size, 'materials');\n }\n } else if (Array.isArray(materials)) {\n console.log('[RevealEffect] Materials is array with length:', materials.length);\n materials.forEach((material: pc.Material) => {\n this._applyShaderToMaterial(material);\n });\n this._shadersApplied = true;\n }\n }\n\n // Also try material property directly\n if (!this._shadersApplied) {\n const material = instance.material || instance._material;\n if (material) {\n console.log('[RevealEffect] Found single material on instance');\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n }\n }\n\n // Subscribe to material:created for future materials\n if (instance.on && !this._materialCreatedHandler) {\n this._materialCreatedHandler = (material: pc.Material) => {\n console.log('[RevealEffect] material:created event received');\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n };\n instance.on('material:created', this._materialCreatedHandler);\n console.log('[RevealEffect] Subscribed to material:created event');\n }\n },\n\n _applyShaderToMaterial(this: any, material: pc.Material) {\n if (this._materialsApplied.has(material)) {\n console.log('[RevealEffect] Material already has shader applied, skipping');\n return;\n }\n\n console.log('[RevealEffect] Applying shader to material:', material);\n console.log('[RevealEffect] Material constructor:', material.constructor?.name);\n console.log('[RevealEffect] Material keys:', Object.keys(material));\n\n // Log all methods on the material\n const methods: string[] = [];\n let obj = material;\n while (obj && obj !== Object.prototype) {\n const names = Object.getOwnPropertyNames(obj);\n for (const name of names) {\n try {\n if (typeof (obj as any)[name] === 'function' && !methods.includes(name)) {\n methods.push(name);\n }\n } catch (e) {}\n }\n obj = Object.getPrototypeOf(obj);\n }\n console.log('[RevealEffect] Material methods:', methods.filter(m => !m.startsWith('_')).join(', '));\n\n const glsl = this.getShaderGLSL();\n const wgsl = this.getShaderWGSL();\n\n // Try setShaderChunk first (newer API)\n const hasSetShaderChunk = typeof (material as any).setShaderChunk === 'function';\n console.log('[RevealEffect] material.setShaderChunk exists:', hasSetShaderChunk);\n\n if (hasSetShaderChunk) {\n if (glsl) {\n (material as any).setShaderChunk('gsplatEffectGLSL', glsl);\n console.log('[RevealEffect] GLSL shader chunk set via setShaderChunk');\n }\n if (wgsl) {\n (material as any).setShaderChunk('gsplatEffectWGSL', wgsl);\n console.log('[RevealEffect] WGSL shader chunk set via setShaderChunk');\n }\n } else {\n // Try alternative: chunks property (older API)\n if ((material as any).chunks) {\n console.log('[RevealEffect] Material has chunks property');\n (material as any).chunks.gsplatEffectGLSL = glsl;\n (material as any).chunks.gsplatEffectWGSL = wgsl;\n console.log('[RevealEffect] Set chunks directly');\n }\n\n // Try: customFragmentShader or options\n if ((material as any).options) {\n console.log('[RevealEffect] Material has options:', (material as any).options);\n }\n\n // Try: shader property\n if ((material as any).shader) {\n console.log('[RevealEffect] Material has shader:', (material as any).shader);\n }\n\n // Try: setParameter for uniforms (this should work)\n if (typeof material.setParameter === 'function') {\n console.log('[RevealEffect] material.setParameter exists - will use for uniforms');\n }\n }\n\n material.update?.();\n this._materialsApplied.add(material);\n console.log('[RevealEffect] Material added to applied set, total:', this._materialsApplied.size);\n },\n\n _removeShaders(this: any) {\n if (this._materialsApplied) {\n this._materialsApplied.forEach((material: pc.Material) => {\n (material as any).setShaderChunk?.('gsplatEffectGLSL', '');\n (material as any).setShaderChunk?.('gsplatEffectWGSL', '');\n material.update?.();\n });\n this._materialsApplied.clear();\n }\n this._shadersApplied = false;\n\n // Clean up instance-level handler (non-unified mode)\n if (this._materialCreatedHandler) {\n const gsplatComponent = this.entity.gsplat;\n const gsplatInstance = (gsplatComponent as any)?.instance;\n if (gsplatInstance?.off) {\n gsplatInstance.off('material:created', this._materialCreatedHandler);\n }\n this._materialCreatedHandler = null;\n }\n\n // Clean up system-level handler (unified mode)\n if (this._systemMaterialHandler) {\n const gsplatSystem = this.app?.systems?.gsplat;\n if (gsplatSystem?.off) {\n gsplatSystem.off('material:created', this._systemMaterialHandler);\n }\n this._systemMaterialHandler = null;\n }\n },\n\n _setUniform(this: any, name: string, value: number | number[]) {\n if (this._materialsApplied) {\n this._materialsApplied.forEach((material: pc.Material) => {\n material.setParameter?.(name, value);\n });\n }\n },\n\n destroy(this: any) {\n this._removeShaders();\n }\n });\n\n _GsplatRevealRadialClass = GsplatRevealRadial;\n return GsplatRevealRadial;\n}\n\n// For backwards compatibility\nexport const GsplatRevealRadial = {\n get class() {\n return getGsplatRevealRadialClass();\n }\n};\n","/**\n * Reveal Effect Presets\n *\n * Pre-configured settings for the radial reveal effect.\n * Users can choose from fast, medium, or slow presets.\n */\n\nexport interface RevealPresetConfig {\n /** Base wave speed in units/second */\n speed: number;\n /** Speed increase over time */\n acceleration: number;\n /** Time offset before lift wave starts (seconds) */\n delay: number;\n /** Position oscillation strength */\n oscillationIntensity: number;\n /** Additive color for initial dots */\n dotTint: { r: number; g: number; b: number };\n /** Additive color for lift wave highlight */\n waveTint: { r: number; g: number; b: number };\n /** Distance at which to disable effect for performance */\n endRadius: number;\n}\n\n/**\n * Reveal effect presets\n */\nexport const REVEAL_PRESETS: Record<string, RevealPresetConfig> = {\n /**\n * Fast preset - Quick reveal for shorter loading experiences\n * Duration: ~3-4 seconds for a 50 unit radius scene\n */\n fast: {\n speed: 10,\n acceleration: 2,\n delay: 0.5,\n oscillationIntensity: 0.1,\n dotTint: { r: 0, g: 1, b: 1 }, // Cyan\n waveTint: { r: 1, g: 0.5, b: 0 }, // Orange\n endRadius: 50\n },\n\n /**\n * Medium preset - Balanced reveal for typical scenes\n * Duration: ~5-7 seconds for a 50 unit radius scene\n */\n medium: {\n speed: 5,\n acceleration: 0,\n delay: 2,\n oscillationIntensity: 0.2,\n dotTint: { r: 0, g: 1, b: 1 }, // Cyan\n waveTint: { r: 1, g: 0.5, b: 0 }, // Orange\n endRadius: 50\n },\n\n /**\n * Slow preset - Dramatic reveal for showcase/demo scenes\n * Duration: ~12-15 seconds for a 50 unit radius scene\n */\n slow: {\n speed: 3,\n acceleration: 0,\n delay: 3,\n oscillationIntensity: 0.25,\n dotTint: { r: 0, g: 1, b: 1 }, // Cyan\n waveTint: { r: 1, g: 0.5, b: 0 }, // Orange\n endRadius: 50\n }\n};\n\n/**\n * Type for reveal effect preset names\n */\nexport type RevealPreset = 'fast' | 'medium' | 'slow' | 'none';\n\n/**\n * Get preset configuration by name\n * Returns undefined for 'none' or invalid presets\n */\nexport function getRevealPreset(preset: RevealPreset): RevealPresetConfig | undefined {\n if (preset === 'none') {\n return undefined;\n }\n return REVEAL_PRESETS[preset];\n}\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.loop = exports.conditional = exports.parse = void 0;\n\nvar parse = function parse(stream, schema) {\n var result = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n var parent = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : result;\n\n if (Array.isArray(schema)) {\n schema.forEach(function (partSchema) {\n return parse(stream, partSchema, result, parent);\n });\n } else if (typeof schema === 'function') {\n schema(stream, result, parent, parse);\n } else {\n var key = Object.keys(schema)[0];\n\n if (Array.isArray(schema[key])) {\n parent[key] = {};\n parse(stream, schema[key], result, parent[key]);\n } else {\n parent[key] = schema[key](stream, result, parent, parse);\n }\n }\n\n return result;\n};\n\nexports.parse = parse;\n\nvar conditional = function conditional(schema, conditionFunc) {\n return function (stream, result, parent, parse) {\n if (conditionFunc(stream, result, parent)) {\n parse(stream, schema, result, parent);\n }\n };\n};\n\nexports.conditional = conditional;\n\nvar loop = function loop(schema, continueFunc) {\n return function (stream, result, parent, parse) {\n var arr = [];\n var lastStreamPos = stream.pos;\n\n while (continueFunc(stream, result, parent)) {\n var newParent = {};\n parse(stream, schema, result, newParent); // cases when whole file is parsed but no termination is there and stream position is not getting updated as well\n // it falls into infinite recursion, null check to avoid the same\n\n if (stream.pos === lastStreamPos) {\n break;\n }\n\n lastStreamPos = stream.pos;\n arr.push(newParent);\n }\n\n return arr;\n };\n};\n\nexports.loop = loop;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.readBits = exports.readArray = exports.readUnsigned = exports.readString = exports.peekBytes = exports.readBytes = exports.peekByte = exports.readByte = exports.buildStream = void 0;\n\n// Default stream and parsers for Uint8TypedArray data type\nvar buildStream = function buildStream(uint8Data) {\n return {\n data: uint8Data,\n pos: 0\n };\n};\n\nexports.buildStream = buildStream;\n\nvar readByte = function readByte() {\n return function (stream) {\n return stream.data[stream.pos++];\n };\n};\n\nexports.readByte = readByte;\n\nvar peekByte = function peekByte() {\n var offset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;\n return function (stream) {\n return stream.data[stream.pos + offset];\n };\n};\n\nexports.peekByte = peekByte;\n\nvar readBytes = function readBytes(length) {\n return function (stream) {\n return stream.data.subarray(stream.pos, stream.pos += length);\n };\n};\n\nexports.readBytes = readBytes;\n\nvar peekBytes = function peekBytes(length) {\n return function (stream) {\n return stream.data.subarray(stream.pos, stream.pos + length);\n };\n};\n\nexports.peekBytes = peekBytes;\n\nvar readString = function readString(length) {\n return function (stream) {\n return Array.from(readBytes(length)(stream)).map(function (value) {\n return String.fromCharCode(value);\n }).join('');\n };\n};\n\nexports.readString = readString;\n\nvar readUnsigned = function readUnsigned(littleEndian) {\n return function (stream) {\n var bytes = readBytes(2)(stream);\n return littleEndian ? (bytes[1] << 8) + bytes[0] : (bytes[0] << 8) + bytes[1];\n };\n};\n\nexports.readUnsigned = readUnsigned;\n\nvar readArray = function readArray(byteSize, totalOrFunc) {\n return function (stream, result, parent) {\n var total = typeof totalOrFunc === 'function' ? totalOrFunc(stream, result, parent) : totalOrFunc;\n var parser = readBytes(byteSize);\n var arr = new Array(total);\n\n for (var i = 0; i < total; i++) {\n arr[i] = parser(stream);\n }\n\n return arr;\n };\n};\n\nexports.readArray = readArray;\n\nvar subBitsTotal = function subBitsTotal(bits, startIndex, length) {\n var result = 0;\n\n for (var i = 0; i < length; i++) {\n result += bits[startIndex + i] && Math.pow(2, length - i - 1);\n }\n\n return result;\n};\n\nvar readBits = function readBits(schema) {\n return function (stream) {\n var _byte = readByte()(stream); // convert the byte to bit array\n\n\n var bits = new Array(8);\n\n for (var i = 0; i < 8; i++) {\n bits[7 - i] = !!(_byte & 1 << i);\n } // convert the bit array to values based on the schema\n\n\n return Object.keys(schema).reduce(function (res, key) {\n var def = schema[key];\n\n if (def.length) {\n res[key] = subBitsTotal(bits, def.index, def.length);\n } else {\n res[key] = bits[def.index];\n }\n\n return res;\n }, {});\n };\n};\n\nexports.readBits = readBits;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.decompressFrames = exports.decompressFrame = exports.parseGIF = void 0;\n\nvar _gif = _interopRequireDefault(require(\"js-binary-schema-parser/lib/schemas/gif\"));\n\nvar _jsBinarySchemaParser = require(\"js-binary-schema-parser\");\n\nvar _uint = require(\"js-binary-schema-parser/lib/parsers/uint8\");\n\nvar _deinterlace = require(\"./deinterlace\");\n\nvar _lzw = require(\"./lzw\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar parseGIF = function parseGIF(arrayBuffer) {\n var byteData = new Uint8Array(arrayBuffer);\n return (0, _jsBinarySchemaParser.parse)((0, _uint.buildStream)(byteData), _gif[\"default\"]);\n};\n\nexports.parseGIF = parseGIF;\n\nvar generatePatch = function generatePatch(image) {\n var totalPixels = image.pixels.length;\n var patchData = new Uint8ClampedArray(totalPixels * 4);\n\n for (var i = 0; i < totalPixels; i++) {\n var pos = i * 4;\n var colorIndex = image.pixels[i];\n var color = image.colorTable[colorIndex] || [0, 0, 0];\n patchData[pos] = color[0];\n patchData[pos + 1] = color[1];\n patchData[pos + 2] = color[2];\n patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;\n }\n\n return patchData;\n};\n\nvar decompressFrame = function decompressFrame(frame, gct, buildImagePatch) {\n if (!frame.image) {\n console.warn('gif frame does not have associated image.');\n return;\n }\n\n var image = frame.image; // get the number of pixels\n\n var totalPixels = image.descriptor.width * image.descriptor.height; // do lzw decompression\n\n var pixels = (0, _lzw.lzw)(image.data.minCodeSize, image.data.blocks, totalPixels); // deal with interlacing if necessary\n\n if (image.descriptor.lct.interlaced) {\n pixels = (0, _deinterlace.deinterlace)(pixels, image.descriptor.width);\n }\n\n var resultImage = {\n pixels: pixels,\n dims: {\n top: frame.image.descriptor.top,\n left: frame.image.descriptor.left,\n width: frame.image.descriptor.width,\n height: frame.image.descriptor.height\n }\n }; // color table\n\n if (image.descriptor.lct && image.descriptor.lct.exists) {\n resultImage.colorTable = image.lct;\n } else {\n resultImage.colorTable = gct;\n } // add per frame relevant gce information\n\n\n if (frame.gce) {\n resultImage.delay = (frame.gce.delay || 10) * 10; // convert to ms\n\n resultImage.disposalType = frame.gce.extras.disposal; // transparency\n\n if (frame.gce.extras.transparentColorGiven) {\n resultImage.transparentIndex = frame.gce.transparentColorIndex;\n }\n } // create canvas usable imagedata if desired\n\n\n if (buildImagePatch) {\n resultImage.patch = generatePatch(resultImage);\n }\n\n return resultImage;\n};\n\nexports.decompressFrame = decompressFrame;\n\nvar decompressFrames = function decompressFrames(parsedGif, buildImagePatches) {\n return parsedGif.frames.filter(function (f) {\n return f.image;\n }).map(function (f) {\n return decompressFrame(f, parsedGif.gct, buildImagePatches);\n });\n};\n\nexports.decompressFrames = decompressFrames;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _ = require(\"../\");\n\nvar _uint = require(\"../parsers/uint8\");\n\n// a set of 0x00 terminated subblocks\nvar subBlocksSchema = {\n blocks: function blocks(stream) {\n var terminator = 0x00;\n var chunks = [];\n var streamSize = stream.data.length;\n var total = 0;\n\n for (var size = (0, _uint.readByte)()(stream); size !== terminator; size = (0, _uint.readByte)()(stream)) {\n // size becomes undefined for some case when file is corrupted and terminator is not proper \n // null check to avoid recursion\n if (!size) break; // catch corrupted files with no terminator\n\n if (stream.pos + size >= streamSize) {\n var availableSize = streamSize - stream.pos;\n chunks.push((0, _uint.readBytes)(availableSize)(stream));\n total += availableSize;\n break;\n }\n\n chunks.push((0, _uint.readBytes)(size)(stream));\n total += size;\n }\n\n var result = new Uint8Array(total);\n var offset = 0;\n\n for (var i = 0; i < chunks.length; i++) {\n result.set(chunks[i], offset);\n offset += chunks[i].length;\n }\n\n return result;\n }\n}; // global control extension\n\nvar gceSchema = (0, _.conditional)({\n gce: [{\n codes: (0, _uint.readBytes)(2)\n }, {\n byteSize: (0, _uint.readByte)()\n }, {\n extras: (0, _uint.readBits)({\n future: {\n index: 0,\n length: 3\n },\n disposal: {\n index: 3,\n length: 3\n },\n userInput: {\n index: 6\n },\n transparentColorGiven: {\n index: 7\n }\n })\n }, {\n delay: (0, _uint.readUnsigned)(true)\n }, {\n transparentColorIndex: (0, _uint.readByte)()\n }, {\n terminator: (0, _uint.readByte)()\n }]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0xf9;\n}); // image pipeline block\n\nvar imageSchema = (0, _.conditional)({\n image: [{\n code: (0, _uint.readByte)()\n }, {\n descriptor: [{\n left: (0, _uint.readUnsigned)(true)\n }, {\n top: (0, _uint.readUnsigned)(true)\n }, {\n width: (0, _uint.readUnsigned)(true)\n }, {\n height: (0, _uint.readUnsigned)(true)\n }, {\n lct: (0, _uint.readBits)({\n exists: {\n index: 0\n },\n interlaced: {\n index: 1\n },\n sort: {\n index: 2\n },\n future: {\n index: 3,\n length: 2\n },\n size: {\n index: 5,\n length: 3\n }\n })\n }]\n }, (0, _.conditional)({\n lct: (0, _uint.readArray)(3, function (stream, result, parent) {\n return Math.pow(2, parent.descriptor.lct.size + 1);\n })\n }, function (stream, result, parent) {\n return parent.descriptor.lct.exists;\n }), {\n data: [{\n minCodeSize: (0, _uint.readByte)()\n }, subBlocksSchema]\n }]\n}, function (stream) {\n return (0, _uint.peekByte)()(stream) === 0x2c;\n}); // plain text block\n\nvar textSchema = (0, _.conditional)({\n text: [{\n codes: (0, _uint.readBytes)(2)\n }, {\n blockSize: (0, _uint.readByte)()\n }, {\n preData: function preData(stream, result, parent) {\n return (0, _uint.readBytes)(parent.text.blockSize)(stream);\n }\n }, subBlocksSchema]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0x01;\n}); // application block\n\nvar applicationSchema = (0, _.conditional)({\n application: [{\n codes: (0, _uint.readBytes)(2)\n }, {\n blockSize: (0, _uint.readByte)()\n }, {\n id: function id(stream, result, parent) {\n return (0, _uint.readString)(parent.blockSize)(stream);\n }\n }, subBlocksSchema]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0xff;\n}); // comment block\n\nvar commentSchema = (0, _.conditional)({\n comment: [{\n codes: (0, _uint.readBytes)(2)\n }, subBlocksSchema]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0xfe;\n});\nvar schema = [{\n header: [{\n signature: (0, _uint.readString)(3)\n }, {\n version: (0, _uint.readString)(3)\n }]\n}, {\n lsd: [{\n width: (0, _uint.readUnsigned)(true)\n }, {\n height: (0, _uint.readUnsigned)(true)\n }, {\n gct: (0, _uint.readBits)({\n exists: {\n index: 0\n },\n resolution: {\n index: 1,\n length: 3\n },\n sort: {\n index: 4\n },\n size: {\n index: 5,\n length: 3\n }\n })\n }, {\n backgroundColorIndex: (0, _uint.readByte)()\n }, {\n pixelAspectRatio: (0, _uint.readByte)()\n }]\n}, (0, _.conditional)({\n gct: (0, _uint.readArray)(3, function (stream, result) {\n return Math.pow(2, result.lsd.gct.size + 1);\n })\n}, function (stream, result) {\n return result.lsd.gct.exists;\n}), // content frames\n{\n frames: (0, _.loop)([gceSchema, applicationSchema, commentSchema, imageSchema, textSchema], function (stream) {\n var nextCode = (0, _uint.peekByte)()(stream); // rather than check for a terminator, we should check for the existence\n // of an ext or image block to avoid infinite loops\n //var terminator = 0x3B;\n //return nextCode !== terminator;\n\n return nextCode === 0x21 || nextCode === 0x2c;\n })\n}];\nvar _default = schema;\nexports[\"default\"] = _default;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.deinterlace = void 0;\n\n/**\r\n * Deinterlace function from https://github.com/shachaf/jsgif\r\n */\nvar deinterlace = function deinterlace(pixels, width) {\n var newPixels = new Array(pixels.length);\n var rows = pixels.length / width;\n\n var cpRow = function cpRow(toRow, fromRow) {\n var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);\n newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));\n }; // See appendix E.\n\n\n var offsets = [0, 4, 2, 1];\n var steps = [8, 8, 4, 2];\n var fromRow = 0;\n\n for (var pass = 0; pass < 4; pass++) {\n for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {\n cpRow(toRow, fromRow);\n fromRow++;\n }\n }\n\n return newPixels;\n};\n\nexports.deinterlace = deinterlace;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.lzw = void 0;\n\n/**\r\n * javascript port of java LZW decompression\r\n * Original java author url: https://gist.github.com/devunwired/4479231\r\n */\nvar lzw = function lzw(minCodeSize, data, pixelCount) {\n var MAX_STACK_SIZE = 4096;\n var nullCode = -1;\n var npix = pixelCount;\n var available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, i, datum, data_size, first, top, bi, pi;\n var dstPixels = new Array(pixelCount);\n var prefix = new Array(MAX_STACK_SIZE);\n var suffix = new Array(MAX_STACK_SIZE);\n var pixelStack = new Array(MAX_STACK_SIZE + 1); // Initialize GIF data stream decoder.\n\n data_size = minCodeSize;\n clear = 1 << data_size;\n end_of_information = clear + 1;\n available = clear + 2;\n old_code = nullCode;\n code_size = data_size + 1;\n code_mask = (1 << code_size) - 1;\n\n for (code = 0; code < clear; code++) {\n prefix[code] = 0;\n suffix[code] = code;\n } // Decode GIF pixel stream.\n\n\n var datum, bits, count, first, top, pi, bi;\n datum = bits = count = first = top = pi = bi = 0;\n\n for (i = 0; i < npix;) {\n if (top === 0) {\n if (bits < code_size) {\n // get the next byte\n datum += data[bi] << bits;\n bits += 8;\n bi++;\n continue;\n } // Get the next code.\n\n\n code = datum & code_mask;\n datum >>= code_size;\n bits -= code_size; // Interpret the code\n\n if (code > available || code == end_of_information) {\n break;\n }\n\n if (code == clear) {\n // Reset decoder.\n code_size = data_size + 1;\n code_mask = (1 << code_size) - 1;\n available = clear + 2;\n old_code = nullCode;\n continue;\n }\n\n if (old_code == nullCode) {\n pixelStack[top++] = suffix[code];\n old_code = code;\n first = code;\n continue;\n }\n\n in_code = code;\n\n if (code == available) {\n pixelStack[top++] = first;\n code = old_code;\n }\n\n while (code > clear) {\n pixelStack[top++] = suffix[code];\n code = prefix[code];\n }\n\n first = suffix[code] & 0xff;\n pixelStack[top++] = first; // add a new string to the table, but only if space is available\n // if not, just continue with current table until a clear code is found\n // (deferred clear code implementation as per GIF spec)\n\n if (available < MAX_STACK_SIZE) {\n prefix[available] = old_code;\n suffix[available] = first;\n available++;\n\n if ((available & code_mask) === 0 && available < MAX_STACK_SIZE) {\n code_size++;\n code_mask += available;\n }\n }\n\n old_code = in_code;\n } // Pop a pixel off the pixel stack.\n\n\n top--;\n dstPixels[pi++] = pixelStack[top];\n i++;\n }\n\n for (i = pi; i < npix; i++) {\n dstPixels[i] = 0; // clear missing pixels\n }\n\n return dstPixels;\n};\n\nexports.lzw = lzw;","/**\n * Animated GIF Helper for PlayCanvas\n *\n * Uses gifuct-js to parse and decompress GIF frames, then renders them\n * to a PlayCanvas texture with proper transparency support.\n */\n\nimport * as pc from 'playcanvas';\nimport { parseGIF, decompressFrames } from 'gifuct-js';\n\n/**\n * Represents a single GIF frame\n */\ninterface GifFrame {\n dims: {\n width: number;\n height: number;\n top: number;\n left: number;\n };\n patch: Uint8ClampedArray;\n delay: number;\n disposalType?: number;\n}\n\n/**\n * Options for creating an animated GIF texture\n */\nexport interface AnimatedGifOptions {\n /** Auto-play the GIF when loaded */\n autoPlay?: boolean;\n /** Callback when the first frame is ready */\n onReady?: () => void;\n /** Callback on error */\n onError?: (error: Error) => void;\n}\n\n/**\n * Animated GIF texture for PlayCanvas\n * Handles parsing, decompression, and frame-by-frame rendering with transparency\n */\nexport class AnimatedGifTexture {\n private app: pc.Application;\n private url: string;\n private frames: GifFrame[] = [];\n private currentFrameIndex = 0;\n private isPlaying = false;\n private isLoaded = false;\n private lastFrameTime = 0;\n private updateHandler: (() => void) | null = null;\n private options: AnimatedGifOptions;\n\n // Canvas for compositing frames (handles disposal and transparency)\n private canvas: HTMLCanvasElement;\n private ctx: CanvasRenderingContext2D;\n\n // PlayCanvas texture\n public texture: pc.Texture | null = null;\n\n // GIF dimensions\n private gifWidth = 0;\n private gifHeight = 0;\n\n constructor(app: pc.Application, url: string, options: AnimatedGifOptions = {}) {\n this.app = app;\n this.url = url;\n this.options = options;\n\n // Create offscreen canvas for compositing\n this.canvas = document.createElement('canvas');\n this.ctx = this.canvas.getContext('2d', { willReadFrequently: true })!;\n\n // Start loading\n this.load();\n }\n\n /**\n * Load and parse the GIF\n */\n private async load(): Promise<void> {\n try {\n // Fetch GIF data\n const response = await fetch(this.url);\n if (!response.ok) {\n throw new Error(`Failed to fetch GIF: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n\n // Parse GIF\n const gif = parseGIF(buffer);\n this.frames = decompressFrames(gif, true) as unknown as GifFrame[];\n\n if (this.frames.length === 0) {\n throw new Error('GIF has no frames');\n }\n\n // Get dimensions from first frame\n this.gifWidth = gif.lsd.width;\n this.gifHeight = gif.lsd.height;\n\n // Setup canvas\n this.canvas.width = this.gifWidth;\n this.canvas.height = this.gifHeight;\n\n // Create PlayCanvas texture\n this.texture = new pc.Texture(this.app.graphicsDevice, {\n width: this.gifWidth,\n height: this.gifHeight,\n format: pc.PIXELFORMAT_RGBA8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n\n // Draw first frame\n this.drawFrame(0);\n\n this.isLoaded = true;\n\n console.log(`[AnimatedGif] Loaded GIF: ${this.url}, ${this.frames.length} frames, ${this.gifWidth}x${this.gifHeight}`);\n\n // Notify ready\n if (this.options.onReady) {\n this.options.onReady();\n }\n\n // Auto-play if requested\n if (this.options.autoPlay) {\n this.play();\n }\n } catch (error) {\n console.error('[AnimatedGif] Error loading GIF:', error);\n if (this.options.onError) {\n this.options.onError(error instanceof Error ? error : new Error(String(error)));\n }\n }\n }\n\n /**\n * Draw a specific frame to the canvas and update the texture\n */\n private drawFrame(frameIndex: number): void {\n if (!this.texture || frameIndex >= this.frames.length) return;\n\n const frame = this.frames[frameIndex];\n const prevFrame = frameIndex > 0 ? this.frames[frameIndex - 1] : null;\n\n // Handle disposal of previous frame\n if (prevFrame && prevFrame.disposalType === 2) {\n // Restore to background (clear the previous frame area)\n this.ctx.clearRect(\n prevFrame.dims.left,\n prevFrame.dims.top,\n prevFrame.dims.width,\n prevFrame.dims.height\n );\n }\n // disposalType === 3 (restore to previous) is complex and rarely used, skip for now\n\n // Create ImageData from frame patch\n const imageData = new ImageData(\n new Uint8ClampedArray(frame.patch),\n frame.dims.width,\n frame.dims.height\n );\n\n // Create temporary canvas for this frame\n const tempCanvas = document.createElement('canvas');\n tempCanvas.width = frame.dims.width;\n tempCanvas.height = frame.dims.height;\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(imageData, 0, 0);\n\n // Draw frame patch at correct position\n this.ctx.drawImage(\n tempCanvas,\n frame.dims.left,\n frame.dims.top\n );\n\n // Update texture from canvas\n this.updateTexture();\n }\n\n /**\n * Update the PlayCanvas texture from the canvas\n */\n private updateTexture(): void {\n if (!this.texture) return;\n\n // Get pixel data from canvas\n const imageData = this.ctx.getImageData(0, 0, this.gifWidth, this.gifHeight);\n\n // Upload to texture\n const pixels = this.texture.lock();\n if (pixels) {\n pixels.set(imageData.data);\n }\n this.texture.unlock();\n this.texture.upload();\n }\n\n /**\n * Animation update - called each frame when playing\n */\n private update = (): void => {\n if (!this.isPlaying || !this.isLoaded || this.frames.length <= 1) return;\n\n const now = performance.now();\n const frame = this.frames[this.currentFrameIndex];\n const delay = frame.delay || 100; // Default 100ms if not specified\n\n if (now - this.lastFrameTime >= delay) {\n // Advance to next frame\n this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.length;\n this.drawFrame(this.currentFrameIndex);\n this.lastFrameTime = now;\n }\n };\n\n /**\n * Start playing the GIF animation\n */\n public play(): void {\n if (this.isPlaying) return;\n\n this.isPlaying = true;\n this.lastFrameTime = performance.now();\n\n // Register update handler\n if (!this.updateHandler) {\n this.updateHandler = this.update;\n this.app.on('update', this.updateHandler);\n }\n }\n\n /**\n * Pause the GIF animation\n */\n public pause(): void {\n if (!this.isPlaying) return;\n\n this.isPlaying = false;\n\n // Remove update handler\n if (this.updateHandler) {\n this.app.off('update', this.updateHandler);\n this.updateHandler = null;\n }\n }\n\n /**\n * Stop and reset to first frame\n */\n public stop(): void {\n this.pause();\n this.currentFrameIndex = 0;\n if (this.isLoaded) {\n // Clear canvas and redraw first frame\n this.ctx.clearRect(0, 0, this.gifWidth, this.gifHeight);\n this.drawFrame(0);\n }\n }\n\n /**\n * Check if the GIF is currently playing\n */\n public get playing(): boolean {\n return this.isPlaying;\n }\n\n /**\n * Check if the GIF is loaded\n */\n public get loaded(): boolean {\n return this.isLoaded;\n }\n\n /**\n * Clean up resources\n */\n public destroy(): void {\n this.pause();\n\n if (this.texture) {\n this.texture.destroy();\n this.texture = null;\n }\n\n this.frames = [];\n this.isLoaded = false;\n }\n}\n\n/**\n * Helper to detect if a URL is a GIF\n */\nexport function isGifUrl(url: string): boolean {\n const lower = url.toLowerCase();\n // Check extension or content type hint\n return lower.endsWith('.gif') || lower.includes('image/gif') || lower.includes('format=gif');\n}\n","/**\n * HTML Mesh Helper for PlayCanvas\n *\n * Renders HTML elements as textures on 3D meshes using the texElement2D API\n * (HTML-in-Canvas proposal) with fallback to canvas rendering.\n *\n * Based on PlayCanvas PR #7897: https://github.com/playcanvas/engine/pull/7897\n */\n\nimport * as pc from 'playcanvas';\n\n/**\n * Patch PlayCanvas GraphicsDevice to add _isHTMLElementInterface method\n * This is required for HTML-in-Canvas support (texElement2D API)\n *\n * The postinstall patches only modify ESM source files, but production builds\n * may use the bundled playcanvas.js which doesn't have the patches.\n * This runtime patch ensures the method exists regardless of which build is used.\n */\nfunction patchPlayCanvasForHtmlMesh(): void {\n // Get the GraphicsDevice class from pc\n const GraphicsDevice = (pc as any).GraphicsDevice;\n if (!GraphicsDevice) {\n console.warn('[HtmlMeshHelper] Could not find pc.GraphicsDevice to patch');\n return;\n }\n\n // Check if already patched\n if (typeof GraphicsDevice.prototype._isHTMLElementInterface === 'function') {\n return; // Already patched\n }\n\n // Add the _isHTMLElementInterface method\n GraphicsDevice.prototype._isHTMLElementInterface = function(texture: any): boolean {\n return typeof HTMLElement !== 'undefined' &&\n texture instanceof HTMLElement &&\n !(texture instanceof HTMLImageElement) &&\n !(texture instanceof HTMLCanvasElement) &&\n !(texture instanceof HTMLVideoElement);\n };\n\n // Patch _isBrowserInterface to include our new method\n const originalIsBrowserInterface = GraphicsDevice.prototype._isBrowserInterface;\n if (originalIsBrowserInterface) {\n GraphicsDevice.prototype._isBrowserInterface = function(texture: any): boolean {\n return originalIsBrowserInterface.call(this, texture) ||\n this._isHTMLElementInterface(texture);\n };\n }\n\n console.log('[HtmlMeshHelper] Patched PlayCanvas GraphicsDevice for HTML-in-Canvas support');\n}\n\n/**\n * HTML Mesh configuration\n */\nexport interface HtmlMeshConfig {\n /** Unique ID for the mesh */\n id: string;\n /** HTML content to render */\n html: string;\n /** CSS styles for the container (can reference external stylesheets) */\n css?: string;\n /** Position in world space */\n position: { x: number; y: number; z: number };\n /** Rotation in degrees */\n rotation?: { x: number; y: number; z: number };\n /** Scale of the mesh */\n scale?: { x: number; y: number; z: number };\n /** Width in pixels (default: 512) */\n width?: number;\n /** Height in pixels (default: 512) */\n height?: number;\n /** Whether to update the texture every frame (for animations) */\n animated?: boolean;\n /** Update rate in milliseconds (default: 100ms for animated) */\n updateRate?: number;\n /** Whether to face the camera (billboard mode) */\n billboard?: boolean;\n /** Billboard range control (percentage or waypoint based) */\n billboardRange?: {\n type: 'percentage' | 'waypoint';\n start: number;\n end: number;\n };\n /** Visibility range control */\n visibilityRange?: {\n type: 'percentage' | 'waypoint';\n start: number;\n end: number;\n };\n /** Whether to receive shadows */\n receiveShadows?: boolean;\n /** Whether to cast shadows */\n castShadows?: boolean;\n /** Opacity (0-1) */\n opacity?: number;\n /** Whether the mesh is double-sided */\n doubleSided?: boolean;\n}\n\n/**\n * HTML Mesh instance\n */\nexport interface HtmlMeshInstance {\n entity: pc.Entity;\n texture: pc.Texture;\n material: pc.StandardMaterial;\n htmlElement: HTMLElement;\n config: HtmlMeshConfig;\n destroy: () => void;\n update: () => void;\n}\n\n/**\n * Creates and manages HTML meshes in a PlayCanvas scene\n */\nexport class HtmlMeshManager {\n private app: pc.Application;\n private canvas: HTMLCanvasElement;\n private meshes: Map<string, HtmlMeshInstance> = new Map();\n private updateHandler: (() => void) | null = null;\n private useTexElement2D: boolean = false;\n\n constructor(app: pc.Application) {\n this.app = app;\n this.canvas = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n // Apply runtime patch to PlayCanvas for HTML-in-Canvas support\n // This ensures _isHTMLElementInterface exists even if the bundled build is used\n patchPlayCanvasForHtmlMesh();\n\n // Check if texElement2D is supported\n const device = app.graphicsDevice as any;\n this.useTexElement2D = device.supportsTexElement2D === true;\n\n console.log(`[HtmlMeshManager] texElement2D support: ${this.useTexElement2D}`);\n }\n\n /**\n * Create an HTML mesh from config\n */\n createMesh(config: HtmlMeshConfig): HtmlMeshInstance {\n const width = config.width || 512;\n const height = config.height || 512;\n\n // Create HTML element container\n const htmlElement = this.createHtmlElement(config, width, height);\n\n // Create texture\n const texture = this.createTexture(htmlElement, width, height, config);\n\n // Create material\n const material = this.createMaterial(texture, config);\n\n // Create entity with plane mesh\n const entity = this.createEntity(config, material, width, height);\n\n // Setup update logic if animated\n const instance: HtmlMeshInstance = {\n entity,\n texture,\n material,\n htmlElement,\n config,\n destroy: () => this.destroyMesh(config.id),\n update: () => this.updateMeshTexture(config.id)\n };\n\n this.meshes.set(config.id, instance);\n\n // Setup animation updates if needed\n if (config.animated && !this.updateHandler) {\n this.startUpdateLoop();\n }\n\n return instance;\n }\n\n /**\n * Create the HTML element for the mesh\n */\n private createHtmlElement(config: HtmlMeshConfig, width: number, height: number): HTMLElement {\n const container = document.createElement('div');\n container.id = `html-mesh-${config.id}`;\n container.style.width = `${width}px`;\n container.style.height = `${height}px`;\n container.style.position = 'absolute';\n container.style.top = '0';\n container.style.left = '0';\n container.style.pointerEvents = 'none';\n container.style.zIndex = '-1';\n container.style.overflow = 'hidden';\n\n // Add custom CSS if provided\n if (config.css) {\n const style = document.createElement('style');\n style.textContent = config.css;\n container.appendChild(style);\n }\n\n // Set HTML content\n container.innerHTML += config.html;\n\n // For texElement2D, the element must be a child of the canvas\n if (this.useTexElement2D) {\n // Enable layoutsubtree for HTML-in-Canvas support\n this.canvas.setAttribute('layoutsubtree', '');\n this.canvas.setAttribute('data-layoutsubtree', '');\n this.canvas.appendChild(container);\n } else {\n // For fallback, we can keep it hidden in the body\n container.style.visibility = 'hidden';\n document.body.appendChild(container);\n }\n\n return container;\n }\n\n /**\n * Create texture from HTML element\n */\n private createTexture(htmlElement: HTMLElement, width: number, height: number, config: HtmlMeshConfig): pc.Texture {\n const texture = new pc.Texture(this.app.graphicsDevice, {\n width,\n height,\n format: pc.PIXELFORMAT_RGBA8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE,\n name: `htmlMesh-${config.id}`\n });\n\n // Set the HTML element as the texture source\n if (this.useTexElement2D) {\n try {\n texture.setSource(htmlElement as any);\n console.log(`[HtmlMeshManager] Using texElement2D for mesh ${config.id}`);\n } catch (error) {\n console.warn(`[HtmlMeshManager] texElement2D failed, falling back to canvas: ${error}`);\n this.renderToCanvas(texture, htmlElement, width, height);\n }\n } else {\n this.renderToCanvas(texture, htmlElement, width, height);\n }\n\n return texture;\n }\n\n /**\n * Fallback: Render HTML to canvas and use as texture source\n */\n private renderToCanvas(texture: pc.Texture, htmlElement: HTMLElement, width: number, height: number): void {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!;\n\n // Create an SVG foreignObject containing the HTML\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <foreignObject width=\"100%\" height=\"100%\">\n <div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"width:${width}px;height:${height}px;\">\n ${htmlElement.innerHTML}\n </div>\n </foreignObject>\n </svg>\n `;\n\n const img = new Image();\n const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n\n img.onload = () => {\n ctx.drawImage(img, 0, 0);\n URL.revokeObjectURL(url);\n texture.setSource(canvas);\n };\n\n img.onerror = () => {\n // Fallback: just draw a colored rectangle with text\n ctx.fillStyle = '#333';\n ctx.fillRect(0, 0, width, height);\n ctx.fillStyle = '#fff';\n ctx.font = '20px Arial';\n ctx.textAlign = 'center';\n ctx.fillText('HTML Mesh', width / 2, height / 2);\n URL.revokeObjectURL(url);\n texture.setSource(canvas);\n };\n\n img.src = url;\n }\n\n /**\n * Create material for the mesh\n */\n private createMaterial(texture: pc.Texture, config: HtmlMeshConfig): pc.StandardMaterial {\n const material = new pc.StandardMaterial();\n material.diffuseMap = texture;\n material.emissiveMap = texture;\n material.emissive = new pc.Color(0.5, 0.5, 0.5);\n material.opacity = config.opacity ?? 1;\n material.blendType = config.opacity !== undefined && config.opacity < 1 ? pc.BLEND_NORMAL : pc.BLEND_NONE;\n material.cull = config.doubleSided ? pc.CULLFACE_NONE : pc.CULLFACE_BACK;\n material.update();\n return material;\n }\n\n /**\n * Create entity with plane mesh\n */\n private createEntity(config: HtmlMeshConfig, material: pc.StandardMaterial, width: number, height: number): pc.Entity {\n const entity = new pc.Entity(`htmlMesh-${config.id}`);\n\n // Calculate aspect ratio for the plane\n const aspect = width / height;\n const baseSize = 1;\n\n entity.addComponent('render', {\n type: 'plane',\n material,\n castShadows: config.castShadows ?? false,\n receiveShadows: config.receiveShadows ?? false\n });\n\n // Set transform\n entity.setPosition(config.position.x, config.position.y, config.position.z);\n\n const rotation = config.rotation || { x: 0, y: 0, z: 0 };\n entity.setEulerAngles(rotation.x, rotation.y, rotation.z);\n\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n entity.setLocalScale(scale.x * aspect, 1, scale.y);\n\n this.app.root.addChild(entity);\n\n // Setup billboard if requested\n if (config.billboard) {\n this.app.on('update', () => {\n if (!entity.enabled) return;\n const camera = this.app.root.findComponent('camera')?.entity;\n if (camera) {\n entity.lookAt(camera.getPosition());\n }\n });\n }\n\n return entity;\n }\n\n /**\n * Update a mesh's texture\n */\n updateMeshTexture(id: string): void {\n const instance = this.meshes.get(id);\n if (!instance) return;\n\n if (this.useTexElement2D) {\n // For texElement2D, just call upload to refresh\n instance.texture.upload();\n } else {\n // For canvas fallback, re-render\n this.renderToCanvas(\n instance.texture,\n instance.htmlElement,\n instance.config.width || 512,\n instance.config.height || 512\n );\n }\n }\n\n /**\n * Start the update loop for animated meshes\n */\n private startUpdateLoop(): void {\n let lastUpdate: { [id: string]: number } = {};\n\n this.updateHandler = () => {\n const now = performance.now();\n\n this.meshes.forEach((instance, id) => {\n if (!instance.config.animated) return;\n\n const rate = instance.config.updateRate || 100;\n const last = lastUpdate[id] || 0;\n\n if (now - last >= rate) {\n this.updateMeshTexture(id);\n lastUpdate[id] = now;\n }\n });\n };\n\n this.app.on('update', this.updateHandler);\n }\n\n /**\n * Destroy a mesh\n */\n destroyMesh(id: string): void {\n const instance = this.meshes.get(id);\n if (!instance) return;\n\n // Remove entity\n instance.entity.destroy();\n\n // Destroy texture\n instance.texture.destroy();\n\n // Remove HTML element\n instance.htmlElement.remove();\n\n this.meshes.delete(id);\n\n // Stop update loop if no animated meshes left\n const hasAnimated = Array.from(this.meshes.values()).some(m => m.config.animated);\n if (!hasAnimated && this.updateHandler) {\n this.app.off('update', this.updateHandler);\n this.updateHandler = null;\n }\n }\n\n /**\n * Get a mesh instance by ID\n */\n getMesh(id: string): HtmlMeshInstance | undefined {\n return this.meshes.get(id);\n }\n\n /**\n * Update visibility and billboard state based on progress\n * Called from the main viewer to sync with scroll/waypoint progress\n */\n updateVisibility(scrollPercent: number, waypointIndex: number): void {\n this.meshes.forEach((instance) => {\n const config = instance.config;\n\n // Update visibility based on range\n if (config.visibilityRange) {\n const range = config.visibilityRange;\n let visible = true;\n\n if (range.type === 'percentage') {\n visible = scrollPercent >= range.start && scrollPercent <= range.end;\n } else if (range.type === 'waypoint') {\n visible = waypointIndex >= range.start && waypointIndex <= range.end;\n }\n\n instance.entity.enabled = visible;\n }\n\n // Update billboard state based on range\n if (config.billboard && config.billboardRange) {\n const range = config.billboardRange;\n let billboardActive = false;\n\n if (range.type === 'percentage') {\n billboardActive = scrollPercent >= range.start && scrollPercent <= range.end;\n } else if (range.type === 'waypoint') {\n billboardActive = waypointIndex >= range.start && waypointIndex <= range.end;\n }\n\n // Store billboard active state for the update loop\n (instance.entity as any)._billboardActive = billboardActive;\n } else if (config.billboard) {\n // Billboard always active if no range\n (instance.entity as any)._billboardActive = true;\n }\n });\n }\n\n /**\n * Get all mesh instances\n */\n getAllMeshes(): Map<string, HtmlMeshInstance> {\n return this.meshes;\n }\n\n /**\n * Destroy all meshes and cleanup\n */\n destroy(): void {\n this.meshes.forEach((_, id) => this.destroyMesh(id));\n\n if (this.updateHandler) {\n this.app.off('update', this.updateHandler);\n this.updateHandler = null;\n }\n }\n}\n\n/**\n * Setup HTML meshes from config array\n */\nexport function setupHtmlMeshes(\n app: pc.Application,\n htmlMeshes: HtmlMeshConfig[]\n): HtmlMeshManager {\n const manager = new HtmlMeshManager(app);\n\n for (const config of htmlMeshes) {\n manager.createMesh(config);\n }\n\n return manager;\n}\n","/**\n * Custom Script System for PlayCanvas\n *\n * Provides runtime execution of user-defined scripts in the viewer.\n * Adapted from the HTML export system for PlayCanvas engine.\n */\n\nimport * as pc from 'playcanvas';\n\n/**\n * API exposed to custom scripts\n */\nexport interface CustomScriptAPI {\n /** PlayCanvas Application instance */\n app: pc.Application;\n /** Camera entity */\n camera: pc.Entity;\n /** PlayCanvas namespace */\n pc: typeof pc;\n /** Canvas element */\n canvas: HTMLCanvasElement;\n /** Get current scroll/progress percentage (0-1) */\n getScrollPercentage: () => number;\n /** Get current waypoint index */\n getCurrentWaypointIndex: () => number;\n /** Get all hotspot entities */\n getHotspots: () => pc.Entity[];\n /** Get splat mesh entities */\n getSplats: () => pc.Entity[];\n /** Get HTML mesh instances */\n getHTMLMeshes: () => any[];\n /** Register a cleanup function to be called on script disposal */\n registerCleanup?: (fn: () => void) => void;\n}\n\n/**\n * Custom Script System\n *\n * Executes user-provided scripts with access to the viewer API.\n * Includes safety measures like sandboxing and cleanup tracking.\n */\nexport class CustomScriptSystem {\n private customScript: string;\n private api: CustomScriptAPI;\n private isInitialized: boolean = false;\n private scriptCleanup: (() => void)[] = [];\n private lastError: any = null;\n private updateCallbacks: (() => void)[] = [];\n\n constructor(customScript: string) {\n this.customScript = customScript;\n this.api = {} as CustomScriptAPI;\n }\n\n /**\n * Initialize the script system with the viewer API\n */\n initialize(api: CustomScriptAPI): void {\n this.api = {\n ...api,\n registerCleanup: (fn: () => void) => this.addCleanup(fn)\n };\n this.isInitialized = true;\n }\n\n /**\n * Update the script content and re-execute\n */\n updateScript(script: string): void {\n if (script === this.customScript) return;\n this.customScript = script;\n this.execute();\n }\n\n /**\n * Add a cleanup function to be called on disposal\n */\n private addCleanup(fn: () => void): void {\n if (typeof fn === 'function') {\n this.scriptCleanup.push(fn);\n }\n }\n\n /**\n * Sanitize script to remove problematic characters\n */\n private sanitizeScript(script: string): string {\n if (!script) return '';\n let s = typeof (script as any).normalize === 'function' ? (script as any).normalize('NFC') : script;\n // Remove BOM\n s = s.replace(/\\uFEFF/g, '');\n // Replace non-breaking space with regular space\n s = s.replace(/\\u00A0/g, ' ');\n // Replace unicode line/paragraph separators with newlines\n s = s.replace(/[\\u2028\\u2029]/g, '\\n');\n // Remove zero-width characters\n s = s.replace(/[\\u200B-\\u200D\\u2060]/g, '');\n // Normalize curly quotes\n s = s.replace(/[\\u2018\\u2019\\u201B]/g, \"'\").replace(/[\\u201C\\u201D\\u201E]/g, '\"');\n return s;\n }\n\n /**\n * Preprocess script with safety wrapping\n */\n private preprocessScript(script: string): string {\n if (!script) return '';\n let processed = this.sanitizeScript(script).trim().replace(/\\r\\n/g, '\\n');\n\n // Warn about common typo\n if (/console\\/log\\s*\\(/.test(processed)) {\n console.warn(\"[Custom Script] Detected 'console/log(...)'. Did you mean 'console.log(...)'?\");\n processed = processed.replace(/console\\/log\\s*\\(/g, 'console.log(');\n }\n\n // Wrap in try-catch for safety\n processed = \"try {\\n\" + processed + \"\\n} catch (error) {\\n console.error('[Custom Script] Runtime error:', error);\\n}\";\n return processed;\n }\n\n /**\n * Execute the custom script\n */\n execute(): void {\n if (!this.isInitialized || !this.customScript) {\n return;\n }\n\n // Prevent extremely large scripts\n if (this.customScript.length > 200_000) {\n console.warn('[Custom Script] Script too large, aborting execution.');\n return;\n }\n\n this.cleanup();\n const processedScript = this.preprocessScript(this.customScript);\n\n try {\n const app = this.api.app;\n let cancelled = false;\n\n // Watchdog to prevent runaway update callbacks\n const watchdog = () => {\n if (cancelled) return;\n if (this.updateCallbacks.length > 200) {\n console.warn('[Custom Script] Too many update callbacks; further callbacks ignored.');\n cancelled = true;\n } else {\n requestAnimationFrame(watchdog);\n }\n };\n requestAnimationFrame(watchdog);\n\n // Wrapped app with tracked registerUpdate (PlayCanvas equivalent of registerBeforeRender)\n const wrappedApp = Object.create(app);\n wrappedApp.registerUpdate = (callback: () => void) => {\n if (typeof callback !== 'function' || cancelled) return;\n const safeCallback = () => {\n try {\n callback();\n } catch (err) {\n console.error('[Custom Script] Error in update callback:', err);\n }\n };\n this.updateCallbacks.push(safeCallback);\n app.on('update', safeCallback);\n this.addCleanup(() => {\n app.off('update', safeCallback);\n const idx = this.updateCallbacks.indexOf(safeCallback);\n if (idx !== -1) this.updateCallbacks.splice(idx, 1);\n });\n };\n\n // Also support BabylonJS-style API for compatibility\n wrappedApp.registerBeforeRender = wrappedApp.registerUpdate;\n\n // Build limited API passed to user script\n const {\n camera,\n pc: pcNamespace,\n canvas,\n getScrollPercentage,\n getCurrentWaypointIndex,\n getHotspots,\n getSplats,\n getHTMLMeshes\n } = this.api;\n\n // Build the function body\n const body = \"'use strict';\\n\" + processedScript;\n\n // Create the function with sandboxed parameters\n // eslint-disable-next-line no-new-func\n const func = new Function(\n 'app',\n 'camera',\n 'pc',\n 'canvas',\n 'getScrollPercentage',\n 'getCurrentWaypointIndex',\n 'getHotspots',\n 'getSplats',\n 'getHTMLMeshes',\n 'registerCleanup',\n 'registerUpdate',\n 'exports',\n 'module',\n 'require',\n 'globalThis',\n 'window',\n 'document',\n 'self',\n body\n );\n\n // Provide restricted globals (prevent direct window access by shadowing)\n const exports: any = {};\n const module: any = { exports };\n const fakeRequire = () => { throw new Error('require not available in custom script'); };\n const blocked = new Proxy({}, { get: () => undefined, set: () => false });\n\n const possibleReturn = func(\n wrappedApp,\n camera,\n pcNamespace,\n canvas,\n getScrollPercentage,\n getCurrentWaypointIndex,\n getHotspots,\n getSplats,\n getHTMLMeshes,\n (fn: () => void) => this.addCleanup(fn),\n wrappedApp.registerUpdate.bind(wrappedApp),\n exports,\n module,\n fakeRequire,\n blocked,\n undefined,\n undefined,\n undefined\n );\n\n // Check for returned cleanup function\n const cleanupCandidate = possibleReturn || module.exports || exports.default || exports.cleanup;\n if (typeof cleanupCandidate === 'function') {\n this.addCleanup(cleanupCandidate);\n }\n\n console.log('[Custom Script] Executed successfully');\n } catch (error) {\n this.lastError = error;\n console.error('[Custom Script] Execution error:', error);\n }\n }\n\n /**\n * Run all cleanup functions\n */\n cleanup(): void {\n this.scriptCleanup.forEach(fn => {\n try {\n fn();\n } catch (error) {\n console.error('[Custom Script] Cleanup error:', error);\n }\n });\n this.scriptCleanup = [];\n this.updateCallbacks = [];\n }\n\n /**\n * Get the last error that occurred during execution\n */\n getLastError(): any {\n return this.lastError;\n }\n\n /**\n * Dispose of the script system and cleanup\n */\n dispose(): void {\n this.cleanup();\n this.isInitialized = false;\n }\n}\n\n/**\n * Setup custom script system with the viewer\n */\nexport function setupCustomScript(\n app: pc.Application,\n camera: pc.Entity,\n canvas: HTMLCanvasElement,\n customScript: string,\n getScrollPercentage: () => number,\n getCurrentWaypointIndex: () => number,\n getHotspots: () => pc.Entity[],\n getSplats: () => pc.Entity[],\n getHTMLMeshes: () => any[]\n): CustomScriptSystem | null {\n if (!customScript || customScript.trim() === '') {\n console.log('[Custom Script] No custom script provided');\n return null;\n }\n\n console.log('[Custom Script] Initializing custom script system...');\n console.log('[Custom Script] Script length:', customScript.length);\n\n const scriptSystem = new CustomScriptSystem(customScript);\n\n scriptSystem.initialize({\n app,\n camera,\n pc,\n canvas,\n getScrollPercentage,\n getCurrentWaypointIndex,\n getHotspots,\n getSplats,\n getHTMLMeshes\n });\n\n scriptSystem.execute();\n\n return scriptSystem;\n}\n","/**\n * Dynamic Viewer for StorySplat\n *\n * Creates an embedded PlayCanvas viewer from scene data.\n * Uses the same core logic as HTML export for 100% parity.\n */\n\nimport * as pc from 'playcanvas';\nimport { transformSceneToExportProps, exportPropsToViewerConfig } from '../transformers/sceneToConfig';\nimport type { SceneData, ViewerInstance, ViewerOptions, ViewerEvent } from '../types';\nimport { createUIElements, connectUIToViewer, injectStyles, hidePreloader, updatePreloaderProgress, showHotspotPopup, setJoystickVisible, updateJoystickPosition, setCameraModeToggleVisible, updateCameraModeToggle, setReturnWaypointButtonVisible, createLazyLoadUI, type UIElements } from './viewerUI';\nimport { CameraControls } from './CameraControls';\nimport { CharacterController } from './CharacterController';\nimport { getGsplatRevealRadialClass, getRevealPreset, type RevealPreset } from '../effects';\nimport { AnimatedGifTexture, isGifUrl } from './AnimatedGifHelper';\nimport { setupHtmlMeshes, type HtmlMeshConfig } from './HtmlMeshHelper';\nimport { setupCustomScript, CustomScriptSystem } from './CustomScriptSystem';\n\n// Event emitter for viewer events\nclass EventEmitter {\n private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();\n\n on(event: string, callback: (...args: any[]) => void): void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(callback);\n }\n\n off(event: string, callback: (...args: any[]) => void): void {\n this.listeners.get(event)?.delete(callback);\n }\n\n emit(event: string, ...args: any[]): void {\n this.listeners.get(event)?.forEach(cb => cb(...args));\n }\n}\n\n// Mobile detection utility\nfunction isMobileDevice(): boolean {\n // Only check user agent - don't use maxTouchPoints as many laptops have touch\n const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera || '';\n return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);\n}\n\n// LOD (Level of Detail) presets for different device capabilities\n// Based on official PlayCanvas GSplat LOD streaming example\nconst LOD_PRESETS = {\n 'desktop-max': {\n range: [0, 5] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n },\n 'desktop': {\n range: [0, 2] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n },\n 'mobile-max': {\n range: [1, 2] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n },\n 'mobile': {\n range: [2, 5] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n }\n} as const;\n\ntype LodPresetName = keyof typeof LOD_PRESETS;\n\n// Helper to detect if URL points to LOD streaming format\nfunction isLodStreamingFormat(url: string): boolean {\n return url.includes('lod-meta.json');\n}\n\n/**\n * Create an embedded viewer from scene data\n */\nexport function createViewer(\n container: HTMLElement,\n scene: SceneData,\n options: ViewerOptions = {}\n): ViewerInstance {\n // Handle lazy loading - show thumbnail with start button before loading viewer\n if (options.lazyLoad) {\n const lazyEvents = new EventEmitter();\n let actualInstance: ViewerInstance | null = null;\n\n // Determine thumbnail URL and button text\n const thumbnailUrl = options.lazyLoadThumbnail || scene.thumbnailUrl;\n const buttonText = options.lazyLoadButtonText || 'Start Experience';\n const uiColor = scene.uiColor || '#4CAF50';\n\n console.log('[StorySplat Viewer] Lazy loading enabled, showing start button');\n\n // Create lazy load UI\n createLazyLoadUI(container, {\n thumbnailUrl,\n buttonText,\n uiColor,\n onStart: () => {\n console.log('[StorySplat Viewer] User clicked start, initializing viewer...');\n // Create actual viewer without lazy loading\n actualInstance = createViewer(container, scene, { ...options, lazyLoad: false });\n\n // Proxy events from actual instance to lazy instance\n actualInstance.on('ready', () => lazyEvents.emit('ready'));\n actualInstance.on('error', (err) => lazyEvents.emit('error', err));\n actualInstance.on('waypointChange', (data) => lazyEvents.emit('waypointChange', data));\n actualInstance.on('playbackStart', () => lazyEvents.emit('playbackStart'));\n actualInstance.on('playbackStop', () => lazyEvents.emit('playbackStop'));\n actualInstance.on('loaded', () => lazyEvents.emit('loaded'));\n actualInstance.on('progress', (data) => lazyEvents.emit('progress', data));\n }\n });\n\n // Return deferred ViewerInstance - methods delegate to actual instance when available\n const deferredInstance: ViewerInstance = {\n app: null as any,\n canvas: null as any,\n\n goToWaypoint: (index) => actualInstance?.goToWaypoint(index),\n nextWaypoint: () => actualInstance?.nextWaypoint(),\n prevWaypoint: () => actualInstance?.prevWaypoint(),\n getCurrentWaypointIndex: () => actualInstance?.getCurrentWaypointIndex() ?? 0,\n getWaypointCount: () => actualInstance?.getWaypointCount() ?? 0,\n\n setPosition: (x, y, z) => actualInstance?.setPosition(x, y, z),\n setRotation: (x, y, z) => actualInstance?.setRotation(x, y, z),\n getPosition: () => actualInstance?.getPosition() ?? { x: 0, y: 0, z: 0 },\n getRotation: () => actualInstance?.getRotation() ?? { x: 0, y: 0, z: 0 },\n\n play: () => actualInstance?.play(),\n pause: () => actualInstance?.pause(),\n stop: () => actualInstance?.stop(),\n isPlaying: () => actualInstance?.isPlaying() ?? false,\n\n destroy: () => {\n if (actualInstance) {\n actualInstance.destroy();\n } else {\n // Clean up lazy load UI if viewer wasn't started\n container.querySelectorAll('.storysplat-lazy-load-container, .storysplat-viewer-container').forEach(el => el.remove());\n container.classList.remove('storysplat-viewer-container');\n }\n },\n resize: () => actualInstance?.resize(),\n\n navigateToScene: async (sceneId) => {\n if (actualInstance) {\n return actualInstance.navigateToScene(sceneId);\n }\n },\n\n on: (event, callback) => lazyEvents.on(event, callback),\n off: (event, callback) => lazyEvents.off(event, callback)\n };\n\n return deferredInstance;\n }\n\n const events = new EventEmitter();\n const config = exportPropsToViewerConfig(transformSceneToExportProps(scene));\n\n console.log('[StorySplat Viewer] Creating viewer with config:', config);\n console.log('[StorySplat Viewer] Scale config:', config.scale);\n console.log('[StorySplat Viewer] Raw scene data:', { splatScale: scene.splatScale, scale: scene.scale });\n\n // Extract UI options from config (respecting JSON settings)\n const showUI = options.showUI !== false; // Default to true\n const uiColor = config.uiColor || '#4CAF50';\n const uiOpts = config.uiOptions || {};\n\n // Map camera modes from export format to UI format\n const mapCameraMode = (mode: string) => {\n if (mode === 'first-person') return 'tour';\n if (mode === 'drone') return 'explore';\n return mode;\n };\n\n // Check if collision meshes are available for walk mode\n const hasCollisionMeshesForWalk = config.collisionMeshesData && config.collisionMeshesData.length > 0;\n\n let allowedModes = (config.allowedCameraModes || ['orbit', 'first-person', 'drone'])\n .map(mapCameraMode)\n .filter((v, i, a) => a.indexOf(v) === i); // unique\n\n // Add 'walk' mode if collision meshes are available and it's in the allowed modes list\n // OR automatically add it if collision meshes exist (for backwards compatibility)\n if (hasCollisionMeshesForWalk && !allowedModes.includes('walk')) {\n allowedModes.push('walk');\n }\n // Remove 'walk' if no collision meshes (can't walk without collisions)\n if (!hasCollisionMeshesForWalk && allowedModes.includes('walk')) {\n allowedModes = allowedModes.filter(m => m !== 'walk');\n }\n\n const defaultMode = mapCameraMode(config.defaultCameraMode || 'orbit');\n\n let uiElements: UIElements = {};\n\n // Create UI elements FIRST (to show preloader while loading)\n if (showUI) {\n uiElements = createUIElements(container, config, {\n uiColor,\n showScrollControls: config.waypoints && config.waypoints.length > 0,\n showModeToggle: allowedModes.length > 1,\n showFullscreenButton: !uiOpts.hideFullscreenButton,\n showHelpButton: !uiOpts.hideHelpButton && !uiOpts.hideInfoButton,\n showPreloader: true,\n allowedCameraModes: allowedModes,\n defaultCameraMode: defaultMode,\n buttonLabels: uiOpts.buttonLabels,\n // Whitelabeling options for preloader\n customPreloaderLogoUrl: uiOpts.customPreloaderLogoUrl,\n // Whitelabeling options for watermark\n hideWatermark: uiOpts.hideWatermark,\n watermarkText: uiOpts.watermarkText,\n watermarkLink: uiOpts.watermarkLink,\n sceneId: scene.sceneId\n });\n }\n\n // Create canvas\n const canvas = document.createElement('canvas');\n canvas.id = 'storysplat-viewer-canvas';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n // Initialize PlayCanvas with WebGL fallback chain\n // Try WebGL2 first, then WebGL1 if that fails\n let app: pc.Application;\n\n // Graphics device options with fallback support\n const baseGraphicsOptions = {\n antialias: false,\n alpha: false,\n powerPreference: 'high-performance' as const\n };\n\n try {\n // First try with default settings (WebGL2 preferred)\n app = new pc.Application(canvas, {\n graphicsDeviceOptions: baseGraphicsOptions,\n mouse: new pc.Mouse(canvas),\n touch: new pc.TouchDevice(canvas),\n keyboard: new pc.Keyboard(window)\n });\n console.log('[StorySplat Viewer] Graphics initialized successfully');\n } catch (webgl2Error) {\n console.warn('[StorySplat Viewer] WebGL2 initialization failed, trying WebGL1 fallback:', webgl2Error);\n\n try {\n // Fallback to WebGL1\n app = new pc.Application(canvas, {\n graphicsDeviceOptions: {\n ...baseGraphicsOptions,\n preferWebGl2: false\n },\n mouse: new pc.Mouse(canvas),\n touch: new pc.TouchDevice(canvas),\n keyboard: new pc.Keyboard(window)\n });\n console.log('[StorySplat Viewer] WebGL1 fallback successful');\n } catch (webgl1Error) {\n console.error('[StorySplat Viewer] WebGL initialization failed completely:', webgl1Error);\n\n // Show user-friendly error message using safe DOM methods\n const errorDiv = document.createElement('div');\n errorDiv.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:rgba(0,0,0,0.8);padding:20px;border-radius:10px;text-align:center;font-family:sans-serif;';\n\n const heading = document.createElement('h3');\n heading.style.cssText = 'margin:0 0 10px 0;';\n heading.textContent = 'Unable to Initialize 3D Graphics';\n\n const message = document.createElement('p');\n message.style.cssText = 'margin:0;';\n message.textContent = 'Your browser or device may not support WebGL. Please try a different browser or device.';\n\n errorDiv.appendChild(heading);\n errorDiv.appendChild(message);\n container.appendChild(errorDiv);\n\n throw new Error('WebGL initialization failed - browser may not support WebGL');\n }\n }\n\n // Handle WebGL context loss\n canvas.addEventListener('webglcontextlost', (e) => {\n e.preventDefault();\n console.error('[StorySplat Viewer] WebGL context lost');\n events.emit('error', new Error('WebGL context lost'));\n }, false);\n\n canvas.addEventListener('webglcontextrestored', () => {\n console.log('[StorySplat Viewer] WebGL context restored');\n // PlayCanvas should handle context restoration automatically\n }, false);\n\n app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);\n app.setCanvasResolution(pc.RESOLUTION_AUTO);\n\n // Start the app first (before adding entities)\n app.start();\n console.log('[StorySplat Viewer] App started');\n\n // Configure GSplat LOD system for optimal performance\n // This improves rendering for all splat formats (PLY, SOG, LOD)\n const isMobile = isMobileDevice();\n const lodPresetName: LodPresetName = isMobile ? 'mobile' : 'desktop';\n const lodPreset = LOD_PRESETS[lodPresetName];\n\n // Scene-level GSplat settings (applies to all gsplat components)\n if (app.scene.gsplat) {\n // LOD update settings - controls how frequently LOD is recalculated\n app.scene.gsplat.lodUpdateAngle = 90; // Angle change threshold for LOD update\n app.scene.gsplat.lodBehindPenalty = 2; // Penalty multiplier for splats behind camera\n app.scene.gsplat.radialSorting = true; // Enable radial sorting for better quality\n app.scene.gsplat.lodUpdateDistance = 1; // Distance change threshold for LOD update\n app.scene.gsplat.lodUnderfillLimit = 10; // Limit for underfill detection\n\n // LOD range settings - controls which LOD levels are used\n app.scene.gsplat.lodRangeMin = lodPreset.range[0];\n app.scene.gsplat.lodRangeMax = lodPreset.range[1];\n\n // SH (Spherical Harmonics) update settings for better color accuracy\n app.scene.gsplat.colorUpdateDistance = 1;\n app.scene.gsplat.colorUpdateAngle = 4;\n app.scene.gsplat.colorUpdateDistanceLodScale = 2;\n app.scene.gsplat.colorUpdateAngleLodScale = 2;\n\n console.log('[StorySplat Viewer] GSplat LOD configured:', {\n preset: lodPresetName,\n lodRange: lodPreset.range,\n isMobile\n });\n }\n\n // State\n let currentWaypointIndex = 0;\n let isPlaying = false;\n let splatEntity: pc.Entity | null = null;\n let revealScript: any = null; // Reference to reveal effect script\n let isDestroyed = false; // Flag to prevent async operations after destroy\n\n // SplatSwap system state\n const additionalSplats = config.additionalSplats || [];\n const keepMeshesInMemory = config.keepMeshesInMemory ?? false;\n let currentSplatUrl: string | null = null;\n let isLoadingSplat = false;\n let initialSplatLoadDone = false;\n const preloadedSplats = new Map<string, pc.Entity>(); // Map URL -> splat entity\n let lastSplatCheckProgress = -1;\n let lastSplatCheckWaypointIndex = -1;\n\n // Create camera (matching HTML export CONFIG values)\n const camera = new pc.Entity('camera');\n\n // Parse background color from config (supports hex strings like \"#1a1a1a\")\n // Default to dark gray (0.1, 0.1, 0.1) if not specified\n let clearColor = new pc.Color(0.1, 0.1, 0.1);\n if (config.backgroundColor) {\n // Parse hex color string to RGB\n const hex = config.backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16) / 255;\n const g = parseInt(hex.substring(2, 4), 16) / 255;\n const b = parseInt(hex.substring(4, 6), 16) / 255;\n clearColor = new pc.Color(r, g, b);\n console.log('[StorySplat Viewer] Background color set from config:', config.backgroundColor);\n }\n }\n\n camera.addComponent('camera', {\n clearColor,\n fov: config.fov || 60,\n nearClip: config.nearClip || 0.1,\n farClip: config.farClip || 1000\n });\n\n // Add audio listener for spatial audio (required for positional sound)\n camera.addComponent('audiolistener');\n\n console.log('[StorySplat Viewer] Camera settings:', {\n fov: config.fov,\n nearClip: config.nearClip,\n farClip: config.farClip,\n playerHeight: config.playerHeight\n });\n\n // Set initial camera position - matching HTML export behavior\n // If waypoints exist, set camera DIRECTLY to first waypoint to avoid 1-frame flash\n const playerHeight = config.playerHeight || 1.6;\n\n if (config.waypoints && config.waypoints.length > 0) {\n const wp = config.waypoints[0];\n console.log('[StorySplat Viewer] First waypoint raw:', wp);\n\n // Set position SYNCHRONOUSLY to avoid camera flash\n // Convert BabylonJS (left-handed) to PlayCanvas (right-handed) - negate Z\n if (wp.position) {\n const pos = wp.position;\n camera.setPosition(pos.x, pos.y, -pos.z);\n console.log('[StorySplat Viewer] Camera position (Z negated):', { x: pos.x, y: pos.y, z: -pos.z });\n } else {\n camera.setPosition(0, playerHeight, 5);\n }\n\n // Set rotation SYNCHRONOUSLY\n if (wp.rotation) {\n const finalRot = convertWaypointRotation(wp.rotation);\n camera.setRotation(finalRot);\n console.log('[StorySplat Viewer] Camera rotation set from waypoint');\n }\n } else {\n // No waypoints - use default position\n camera.setPosition(0, playerHeight, 5);\n camera.lookAt(new pc.Vec3(0, 0, 0));\n console.log('[StorySplat Viewer] Camera set to default position (0, playerHeight, 5)');\n }\n\n app.root.addChild(camera);\n\n // Add directional light (matching HTML export)\n const light = new pc.Entity('light');\n light.addComponent('light', {\n type: pc.LIGHTTYPE_DIRECTIONAL,\n color: new pc.Color(1, 1, 1),\n intensity: 1,\n castShadows: false\n });\n light.setEulerAngles(45, 45, 0);\n app.root.addChild(light);\n console.log('[StorySplat Viewer] Light added');\n\n // Create CameraControls for explore mode (orbit + fly + focus)\n // Movement and rotation speeds tuned down for smoother, more controlled camera\n const cameraControls = new CameraControls(camera, app, {\n moveSpeed: (config.cameraMovementSpeed || 1) * 2, // Reduced from 5x to 2x\n moveFastSpeed: (config.cameraMovementSpeed || 1) * 5, // Reduced from 10x to 5x\n moveSlowSpeed: (config.cameraMovementSpeed || 1) * 1, // Reduced from 2.5x to 1x\n rotateSpeed: (config.cameraRotationSensitivity || 0.2) * 0.0005, // Halved from 0.001 to 0.0005\n enableOrbit: true,\n enableFly: true,\n enablePan: true\n });\n\n // Create CharacterController for walk mode (first-person with collisions)\n let characterController: CharacterController | null = null;\n const hasCollisionMeshes = config.collisionMeshesData && config.collisionMeshesData.length > 0;\n\n if (hasCollisionMeshes) {\n // CharacterController for walk mode - tuned down for smoother movement\n characterController = new CharacterController(camera, app, {\n moveSpeed: (config.cameraMovementSpeed || 1) * 2, // Reduced from 4x to 2x\n sprintMultiplier: 2,\n lookSensitivity: (config.cameraRotationSensitivity || 0.2) * 0.005, // Halved from 0.01 to 0.005\n playerHeight: config.playerHeight || 1.6,\n gravity: 20,\n jumpVelocity: 8,\n collisionRadius: 0.3,\n stepHeight: 0.3\n });\n\n // Load collision meshes asynchronously\n characterController.createCollisionMeshes(config.collisionMeshesData!).then(() => {\n console.log('[StorySplat Viewer] Collision meshes loaded for walk mode');\n }).catch((err) => {\n console.error('[StorySplat Viewer] Failed to load collision meshes:', err);\n });\n }\n\n // Track current camera mode\n let currentCameraMode = defaultMode;\n let waypointControlEnabled = true; // When false, waypoint navigation doesn't control camera\n\n // Per-waypoint orbit mode state (for tour mode with orbit waypoints)\n let currentWaypointOrbitEnabled = false;\n let currentWaypointOrbitTarget: pc.Vec3 | null = null;\n\n // Disable controls initially if in tour mode\n if (defaultMode === 'tour') {\n cameraControls.disable();\n }\n\n // Create picker for GSplat picking (double-click to focus)\n // Enable depth=true for getWorldPointAsync to work with splats (requires PlayCanvas 2.14+)\n const picker = new pc.Picker(app, 1, 1, true);\n\n // XR (VR/AR) Support\n let isInXR = false;\n let xrSessionType: 'vr' | 'ar' | null = null;\n\n // Setup XR if enabled in config\n function setupXR(): void {\n if (!config.includeXR) return;\n\n // Check if XR is supported\n if (!app.xr) {\n console.warn('[StorySplat Viewer] WebXR not supported in this browser');\n return;\n }\n\n const xr = app.xr; // Non-null reference for TypeScript\n const xrMode = config.xrMode || 'both';\n\n // Check VR availability\n if (xrMode === 'vr' || xrMode === 'both') {\n if (xr.isAvailable(pc.XRTYPE_VR)) {\n uiElements.vrButton?.classList.add('available');\n console.log('[StorySplat Viewer] VR is available');\n }\n\n // Listen for VR availability changes\n xr.on('available:' + pc.XRTYPE_VR, (available: boolean) => {\n if (available) {\n uiElements.vrButton?.classList.add('available');\n } else {\n uiElements.vrButton?.classList.remove('available');\n }\n });\n }\n\n // Check AR availability\n if (xrMode === 'ar' || xrMode === 'both') {\n if (xr.isAvailable(pc.XRTYPE_AR)) {\n uiElements.arButton?.classList.add('available');\n console.log('[StorySplat Viewer] AR is available');\n }\n\n // Listen for AR availability changes\n xr.on('available:' + pc.XRTYPE_AR, (available: boolean) => {\n if (available) {\n uiElements.arButton?.classList.add('available');\n } else {\n uiElements.arButton?.classList.remove('available');\n }\n });\n }\n\n // Handle XR session start\n xr.on('start', () => {\n isInXR = true;\n console.log('[StorySplat Viewer] XR session started');\n\n // Update button states\n if (xrSessionType === 'vr') {\n uiElements.vrButton?.classList.add('active');\n uiElements.vrButton!.textContent = 'Exit VR';\n } else if (xrSessionType === 'ar') {\n uiElements.arButton?.classList.add('active');\n uiElements.arButton!.textContent = 'Exit AR';\n }\n\n // Disable regular camera controls in XR\n cameraControls.disable();\n if (characterController) {\n characterController.disable();\n }\n\n events.emit('xrStart', { type: xrSessionType });\n });\n\n // Handle XR session end\n xr.on('end', () => {\n isInXR = false;\n console.log('[StorySplat Viewer] XR session ended');\n\n // Reset button states\n uiElements.vrButton?.classList.remove('active');\n uiElements.arButton?.classList.remove('active');\n if (uiElements.vrButton) uiElements.vrButton.textContent = 'VR';\n if (uiElements.arButton) uiElements.arButton.textContent = 'AR';\n\n xrSessionType = null;\n\n // Re-enable camera controls based on current mode\n if (currentCameraMode === 'explore') {\n cameraControls.enable();\n } else if (currentCameraMode === 'walk' && characterController) {\n characterController.enable();\n }\n\n events.emit('xrEnd', {});\n });\n\n // VR button click handler\n if (uiElements.vrButton) {\n uiElements.vrButton.addEventListener('click', () => {\n if (isInXR && xrSessionType === 'vr') {\n // Exit VR\n xr.end();\n } else if (!isInXR && xr.isAvailable(pc.XRTYPE_VR)) {\n // Enter VR\n xrSessionType = 'vr';\n camera.camera!.startXr(pc.XRTYPE_VR, pc.XRSPACE_LOCALFLOOR, {\n callback: (err: Error | null) => {\n if (err) {\n console.error('[StorySplat Viewer] Failed to start VR:', err);\n xrSessionType = null;\n }\n }\n });\n }\n });\n }\n\n // AR button click handler\n if (uiElements.arButton) {\n uiElements.arButton.addEventListener('click', () => {\n if (isInXR && xrSessionType === 'ar') {\n // Exit AR\n xr.end();\n } else if (!isInXR && xr.isAvailable(pc.XRTYPE_AR)) {\n // Enter AR\n xrSessionType = 'ar';\n camera.camera!.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {\n callback: (err: Error | null) => {\n if (err) {\n console.error('[StorySplat Viewer] Failed to start AR:', err);\n xrSessionType = null;\n }\n }\n });\n }\n });\n }\n }\n\n // Add update loop for CameraControls, CharacterController, and proximity checks\n app.on('update', (dt: number) => {\n // Update either CameraControls or CharacterController based on mode\n if (currentCameraMode === 'walk' && characterController) {\n characterController.update(dt);\n } else {\n cameraControls.update(dt);\n }\n\n // Check proximity triggers for video hotspots (needed for explore/walk mode)\n if (!waypointControlEnabled) {\n updateProximityTriggers();\n }\n\n // Update waypoint audio based on proximity (for spatial audio)\n updateWaypointAudioProximity();\n\n // Check waypoint trigger distances for interaction execution\n checkWaypointTriggerDistance();\n });\n\n // Listen to CameraControls joystick events for UI updates\n app.on('joystick:left', (bx: number, by: number, sx: number, sy: number) => {\n if (bx < 0 || by < 0) {\n // Joystick released\n updateJoystickPosition(uiElements, false, 0, 0, 60);\n } else {\n // Joystick active - calculate offset from base to stick\n const dx = sx - bx;\n const dy = sy - by;\n updateJoystickPosition(uiElements, true, dx, dy, 60);\n }\n });\n\n // Camera mode toggle button (Orbit/Fly) for mobile explore mode\n if (uiElements.cameraModeToggle) {\n uiElements.cameraModeToggle.addEventListener('click', () => {\n // Only toggle if in explore mode\n if (currentCameraMode !== 'explore') return;\n\n const currentMode = cameraControls.mode;\n if (currentMode === 'orbit' || currentMode === 'focus') {\n // Switch to fly mode - show joystick\n cameraControls.setMode('fly');\n updateCameraModeToggle(uiElements, 'fly');\n setJoystickVisible(uiElements, true);\n } else {\n // Switch to orbit mode - hide joystick (orbit uses touch gestures, not joystick)\n cameraControls.setMode('orbit');\n updateCameraModeToggle(uiElements, 'orbit');\n setJoystickVisible(uiElements, false);\n }\n });\n }\n\n // Listen for CameraControls internal mode changes (e.g., after double-tap focus)\n app.on('cameracontrols:modechange', (mode: string) => {\n // Update toggle button UI when mode changes internally\n if (mode === 'orbit' || mode === 'fly') {\n updateCameraModeToggle(uiElements, mode as 'orbit' | 'fly');\n // Also update joystick visibility on mobile\n if (isMobile && currentCameraMode === 'explore') {\n setJoystickVisible(uiElements, mode === 'fly');\n }\n }\n });\n\n // Proximity check function (called every frame in explore mode)\n function updateProximityTriggers(): void {\n const cameraPos = camera.getPosition();\n\n hotspotEntities.forEach((entity: any) => {\n const hotspot = entity.hotspotData;\n if (!hotspot || hotspot.type !== 'video' || !entity.videoElement) return;\n\n const triggerMode = entity.mediaTriggerMode || 'click';\n if (triggerMode !== 'proximity') return;\n\n const hotspotPos = entity.getPosition();\n const distance = cameraPos.distance(hotspotPos);\n const proximityDistance = entity.proximityDistance || 5;\n\n if (distance <= proximityDistance && !entity.isVideoPlaying) {\n playVideoHotspot(entity, hotspot);\n } else if (distance > proximityDistance && entity.isVideoPlaying) {\n pauseVideoHotspot(entity);\n }\n });\n }\n\n // Mode switching function (Tour, Explore, or Walk)\n function setCameraMode(mode: string): void {\n // Disable previous mode's controllers\n if (currentCameraMode === 'walk' && characterController) {\n characterController.disable();\n } else if (currentCameraMode === 'explore') {\n cameraControls.disable();\n }\n\n currentCameraMode = mode;\n console.log('[StorySplat Viewer] Switching to mode:', mode);\n\n const isMobile = isMobileDevice();\n\n if (mode === 'walk' && characterController) {\n // Walk mode: Enable CharacterController (first-person with collisions)\n waypointControlEnabled = false;\n\n // Disable CameraControls\n cameraControls.disable();\n\n // Enable CharacterController\n characterController.enable();\n\n // Hide joystick and camera mode toggle (CharacterController uses keyboard/mouse)\n setJoystickVisible(uiElements, false);\n setCameraModeToggleVisible(uiElements, false);\n\n // Show return to waypoint button (if we have waypoints)\n if (config.waypoints && config.waypoints.length > 0) {\n setReturnWaypointButtonVisible(uiElements, true);\n }\n } else if (mode === 'explore') {\n // Explore mode: Enable CameraControls (orbit + fly + pan)\n\n // Disable CharacterController if active\n if (characterController) {\n characterController.disable();\n }\n\n // CRITICAL: Reset user rotation offsets and disable waypoint control\n userYawOffset = 0;\n userPitchOffset = 0;\n isUserDragging = false;\n waypointControlEnabled = false;\n\n // IMPORTANT: Call disable() first to clear any accumulated input from tour mode dragging\n // The CameraControls input sources accumulate mouse/touch input even when disabled,\n // and this would cause an offset when we enable and the update loop reads that input\n cameraControls.disable(); // Clears input buffers\n cameraControls.enable();\n cameraControls.enableOrbit = true;\n cameraControls.enableFly = true;\n cameraControls.enablePan = true;\n\n // Use syncFromPose to directly set the camera from the waypoint target values\n // This bypasses the camera entity's current state which may have user rotation applied\n if (targetCameraPosition && targetCameraRotation) {\n cameraControls.syncFromPose(targetCameraPosition, targetCameraRotation);\n console.log('[StorySplat Viewer] Synced camera from waypoint pose for explore mode');\n } else {\n cameraControls.syncFromCamera();\n }\n\n // Try to find a better focus point using the picker (async improvement)\n const findBetterFocusPoint = async () => {\n try {\n const pickerScale = 0.25;\n picker.resize(\n Math.floor(canvasEl.clientWidth * pickerScale),\n Math.floor(canvasEl.clientHeight * pickerScale)\n );\n\n const worldLayer = app.scene.layers.getLayerByName('World');\n if (worldLayer) {\n picker.prepare(camera.camera!, app.scene, [worldLayer]);\n\n const centerX = Math.floor(canvasEl.clientWidth * 0.5 * pickerScale);\n const centerY = Math.floor(canvasEl.clientHeight * 0.5 * pickerScale);\n\n const worldPoint = await picker.getWorldPointAsync(centerX, centerY);\n if (worldPoint) {\n const cameraPos = camera.getPosition();\n const distance = cameraPos.distance(worldPoint);\n if (distance > 0.5 && distance < 500) {\n cameraControls.syncFromCamera(worldPoint);\n console.log('[StorySplat Viewer] Updated focus target at distance:', distance.toFixed(2));\n }\n }\n }\n } catch (err) {\n // Picking failed, keep default\n }\n };\n findBetterFocusPoint();\n\n // On mobile, show joystick and camera mode toggle, start in fly mode\n // On desktop, start in orbit mode (left-click drag to orbit, right-click for fly)\n if (isMobile) {\n setJoystickVisible(uiElements, true);\n setCameraModeToggleVisible(uiElements, true);\n // Start in fly mode so joystick works immediately\n cameraControls.setMode('fly');\n updateCameraModeToggle(uiElements, 'fly');\n } else {\n // Desktop starts in orbit mode - user can switch to fly by right-clicking\n cameraControls.setMode('orbit');\n }\n\n // Show return to waypoint button in explore mode (if we have waypoints)\n if (config.waypoints && config.waypoints.length > 0) {\n setReturnWaypointButtonVisible(uiElements, true);\n }\n } else {\n // Tour mode - disable camera controls, enable waypoint control\n cameraControls.disable();\n if (characterController) {\n characterController.disable();\n }\n waypointControlEnabled = true;\n // Hide joystick, camera mode toggle, and return button in tour mode\n setJoystickVisible(uiElements, false);\n setCameraModeToggleVisible(uiElements, false);\n setReturnWaypointButtonVisible(uiElements, false);\n }\n\n events.emit('modeChange', { mode });\n }\n\n // Update preloader progress helper (defined early so loadSplat can use it)\n const updateProgress = (progress: number, text?: string) => {\n if (uiElements.preloader) {\n updatePreloaderProgress(uiElements.preloader, progress, text);\n }\n events.emit('progress', { progress, text });\n };\n\n // Load splat\n async function loadSplat(): Promise<void> {\n // Build URL list with priority: LOD streaming > SOG > PLY/Other formats\n // This ensures backward compatibility with all formats while preferring optimal ones\n const urls: string[] = [];\n\n // Highest priority: LOD streaming format (lod-meta.json) if available\n if (config.lodMetaUrl) urls.push(config.lodMetaUrl);\n\n // Second priority: SOG compressed format\n if (config.sogUrl) urls.push(config.sogUrl);\n\n // Third priority: Original format (PLY, SPLAT, etc.)\n if (config.splatUrl) urls.push(config.splatUrl);\n\n // Fallback URLs for additional format support\n if (config.fallbackUrls) urls.push(...config.fallbackUrls);\n\n console.log('[StorySplat Viewer] Loading splat from URLs:', urls);\n console.log('[StorySplat Viewer] URL priorities: LOD streaming > SOG > PLY/Other');\n updateProgress(0.3, 'Loading splat...');\n\n for (const url of urls) {\n if (!url) continue;\n\n try {\n console.log('[StorySplat Viewer] Trying URL:', url);\n\n // Determine asset type based on extension\n const ext = url.split('.').pop()?.toLowerCase() || 'splat';\n const assetType = 'gsplat'; // PlayCanvas uses 'gsplat' for all gaussian splat formats\n\n const asset = new pc.Asset('splat-' + Date.now(), assetType, { url });\n\n // Track asset loading progress\n asset.on('progress', (received: number, total: number) => {\n if (total > 0) {\n // Map progress from 0.3 to 0.9 (leaving 0.9-1.0 for post-load setup)\n const loadProgress = 0.3 + (received / total) * 0.6;\n updateProgress(loadProgress, `Loading... ${Math.round((received / total) * 100)}%`);\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n let hasRejected = false;\n\n // Only listen for SOG parse errors when loading lod-meta.json (SOG LOD streaming format)\n const isLodFormat = url.includes('lod-meta.json');\n\n // Catch unhandled promise rejections during loading (e.g., SOG v1 parse errors)\n // Only for LOD streaming format which uses the SOG parser\n const unhandledRejectionHandler = isLodFormat ? (event: PromiseRejectionEvent) => {\n if (hasRejected) return;\n const errorMsg = event.reason?.message || String(event.reason);\n // Check if this is a SOG parsing error (deprecated v1 format)\n if (errorMsg.includes('shape') || errorMsg.includes('upgradeMeta')) {\n console.warn('[StorySplat Viewer] Caught SOG parse error, will try fallback:', errorMsg);\n hasRejected = true;\n event.preventDefault(); // Prevent console error\n\n // Emit a deprecation warning event for UI to display\n events.emit('warning', {\n type: 'deprecated_format',\n message: 'This scene uses an outdated SOG format. Please re-upload your scene with the latest tools for better performance.',\n details: 'SOG v1 format is deprecated. Use splat-transform v2+ to convert your PLY files.',\n url: url\n });\n\n reject(new Error(`SOG parse error: ${errorMsg}`));\n }\n } : null;\n\n if (unhandledRejectionHandler) {\n window.addEventListener('unhandledrejection', unhandledRejectionHandler);\n }\n\n // Cleanup handler when done\n const cleanup = () => {\n if (unhandledRejectionHandler) {\n window.removeEventListener('unhandledrejection', unhandledRejectionHandler);\n }\n };\n\n // Use asset.ready() like HTML export - this waits for asset AND resources to be ready\n asset.ready(() => {\n // Check if viewer was destroyed while loading\n if (isDestroyed) {\n console.log('[StorySplat Viewer] Ignoring splat load - viewer was destroyed');\n cleanup();\n reject(new Error('Viewer destroyed'));\n return;\n }\n console.log('[StorySplat Viewer] Asset ready (loaded + resources ready)');\n\n try {\n splatEntity = new pc.Entity('splat');\n splatEntity.addComponent('gsplat', {\n asset: asset,\n unified: true // Enable unified sorting with other transparent objects (hotspots)\n });\n\n // Set LOD distances for LOD streaming format\n // This enables distance-based quality switching when using lod-meta.json\n const gs = splatEntity.gsplat as any;\n if (gs && isLodStreamingFormat(url)) {\n gs.lodDistances = lodPreset.lodDistances;\n console.log('[StorySplat Viewer] LOD distances set for streaming format:', lodPreset.lodDistances);\n }\n\n // Apply scale - match BabylonJS behavior exactly\n // In BabylonJS: invertYScale negates Y, and when invertX !== invertY, Z is also negated to maintain handedness\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n const invertX = config.invertXScale || false;\n const invertY = config.invertYScale || false;\n const finalScale = {\n x: invertX ? -scale.x : scale.x,\n y: invertY ? -scale.y : scale.y,\n z: (invertX !== invertY) ? -scale.z : scale.z // Invert Z when only one of X or Y is inverted\n };\n splatEntity.setLocalScale(finalScale.x, finalScale.y, finalScale.z);\n\n // Apply position (negate Z for Babylon->PlayCanvas coordinate conversion)\n const pos = config.position || [0, 0, 0];\n splatEntity.setPosition(pos[0], pos[1], -pos[2]);\n\n // Apply rotation - convert from BabylonJS (left-handed) to PlayCanvas (right-handed)\n const rot = config.rotation || [0, 0, 0];\n const hasUserRotation = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;\n\n let finalRotDeg: number[];\n if (hasUserRotation) {\n // User specified rotation - apply directly (no base correction)\n // Negate Z for coordinate system conversion\n finalRotDeg = [\n rot[0] * (180 / Math.PI),\n rot[1] * (180 / Math.PI),\n -rot[2] * (180 / Math.PI)\n ];\n } else {\n // No user rotation - apply 180° X base correction for splat orientation\n finalRotDeg = [180, 0, 0];\n }\n\n splatEntity.setEulerAngles(finalRotDeg[0], finalRotDeg[1], finalRotDeg[2]);\n\n console.log('[StorySplat Viewer] Splat transform applied:', {\n scale: finalScale,\n position: [pos[0], pos[1], -pos[2]],\n rotation: finalRotDeg,\n hasUserRotation,\n invertXScale: invertX,\n invertYScale: invertY\n });\n\n app.root.addChild(splatEntity);\n console.log('[StorySplat Viewer] Splat entity added to scene');\n\n // Set alphaClip for GSplat picking support (required for pc.Picker to work with splats)\n // Lower value = more splats are pickable (0.1 is very forgiving)\n if (splatEntity.gsplat?.material) {\n splatEntity.gsplat.material.setParameter('alphaClip', 0.1);\n console.log('[StorySplat Viewer] GSplat alphaClip set for picking support');\n }\n\n // Apply reveal effect if configured (but start disabled - will enable after preloader hides)\n const revealPresetName = (options.revealEffect || scene.revealEffect || 'medium') as RevealPreset;\n const revealConfig = getRevealPreset(revealPresetName);\n\n if (revealConfig) {\n // Add script component for reveal effect\n splatEntity.addComponent('script');\n\n // Create the reveal effect (use getter to get class after app is initialized)\n const GsplatRevealRadialClass = getGsplatRevealRadialClass();\n revealScript = (splatEntity.script as any)?.create(GsplatRevealRadialClass);\n if (revealScript) {\n // Start disabled - will be enabled after preloader hides\n revealScript.enabled = false;\n\n // Apply preset configuration\n revealScript.center.set(0, 0, 0);\n revealScript.speed = revealConfig.speed;\n revealScript.acceleration = revealConfig.acceleration;\n revealScript.delay = revealConfig.delay;\n revealScript.oscillationIntensity = revealConfig.oscillationIntensity;\n revealScript.dotTint.set(revealConfig.dotTint.r, revealConfig.dotTint.g, revealConfig.dotTint.b);\n revealScript.waveTint.set(revealConfig.waveTint.r, revealConfig.waveTint.g, revealConfig.waveTint.b);\n revealScript.endRadius = revealConfig.endRadius;\n\n console.log('[StorySplat Viewer] Reveal effect configured (waiting to start):', revealPresetName);\n }\n } else {\n console.log('[StorySplat Viewer] Reveal effect disabled');\n }\n\n // Wait a brief moment to let async texture loading errors surface\n // before resolving (SOG v1 parse errors happen asynchronously)\n setTimeout(() => {\n if (hasRejected) return; // Already rejected by unhandled rejection handler\n cleanup();\n resolve();\n }, 100);\n } catch (syncError) {\n console.error('[StorySplat Viewer] Error during gsplat setup:', syncError);\n cleanup();\n reject(syncError);\n }\n });\n\n asset.on('error', (err: any) => {\n // Enhanced error logging to help diagnose SOG/PLY loading issues\n console.error('[StorySplat Viewer] Asset load error:', {\n url,\n assetType,\n error: err,\n message: err?.message || 'Unknown error',\n status: err?.status || err?.statusCode || 'N/A',\n stack: err?.stack\n });\n cleanup();\n reject(err);\n });\n\n app.assets.add(asset);\n app.assets.load(asset);\n });\n\n events.emit('loaded');\n console.log('[StorySplat Viewer] Splat loaded successfully');\n return;\n } catch (err) {\n console.warn('[StorySplat Viewer] Failed to load:', url, err);\n }\n }\n\n console.error('[StorySplat Viewer] Failed to load splat from any URL');\n events.emit('error', new Error('Failed to load splat from any URL'));\n }\n\n // Helper to convert waypoint rotation to PlayCanvas quaternion\n // Matching POC waypointNavigation.ts exactly - NO yFlip\n function convertWaypointRotation(rot: any): pc.Quat {\n if ('_w' in rot || 'w' in rot) {\n // Quaternion format - BabylonJS to PlayCanvas conversion\n // Negate X and Y for handedness conversion (matching POC exactly)\n const qx = rot._x ?? rot.x ?? 0;\n const qy = rot._y ?? rot.y ?? 0;\n const qz = rot._z ?? rot.z ?? 0;\n const qw = rot._w ?? rot.w ?? 1;\n return new pc.Quat(-qx, -qy, qz, qw);\n } else if ('x' in rot && 'y' in rot && 'z' in rot) {\n // Euler angles\n const result = new pc.Quat();\n result.setFromEulerAngles(rot.x || 0, rot.y || 0, rot.z || 0);\n return result;\n } else {\n return new pc.Quat();\n }\n }\n\n // =========================================================\n // SPLATSWAP SYSTEM\n // Handles swapping between multiple splat files during navigation\n // =========================================================\n\n /**\n * Hide a preloaded splat (Option 1: keep in memory)\n */\n function hideSplat(entity: pc.Entity): void {\n entity.enabled = false;\n console.log('[SplatSwap] Splat hidden');\n }\n\n /**\n * Dispose of a preloaded splat (Option 2: free memory)\n */\n function disposeSplat(url: string): void {\n const entity = preloadedSplats.get(url);\n if (entity) {\n entity.destroy();\n preloadedSplats.delete(url);\n console.log('[SplatSwap] Splat disposed:', url);\n }\n }\n\n /**\n * Show a preloaded splat\n */\n function showSplat(url: string): boolean {\n const entity = preloadedSplats.get(url);\n if (!entity) return false;\n\n entity.enabled = true;\n currentSplatUrl = url;\n console.log('[SplatSwap] Splat shown:', url);\n return true;\n }\n\n /**\n * Preload a splat in the background without displaying it\n */\n async function preloadSplat(url: string): Promise<void> {\n if (preloadedSplats.has(url)) return;\n if (url === currentSplatUrl) return;\n if (isDestroyed) return;\n\n console.log('[SplatSwap] Preloading splat:', url);\n\n try {\n const asset = new pc.Asset('splat-preload-' + Date.now(), 'gsplat', { url });\n\n await new Promise<void>((resolve, reject) => {\n asset.ready(() => {\n if (isDestroyed) {\n reject(new Error('Viewer destroyed'));\n return;\n }\n\n const entity = new pc.Entity('splat-preload');\n entity.addComponent('gsplat', { asset, unified: true });\n\n // Apply scale/position/rotation matching main splat\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n const invertX = config.invertXScale || false;\n const invertY = config.invertYScale || false;\n const finalScale = {\n x: invertX ? -scale.x : scale.x,\n y: invertY ? -scale.y : scale.y,\n z: (invertX !== invertY) ? -scale.z : scale.z\n };\n entity.setLocalScale(finalScale.x, finalScale.y, finalScale.z);\n\n const pos = config.position || [0, 0, 0];\n entity.setPosition(pos[0], pos[1], -pos[2]);\n\n const rot = config.rotation || [0, 0, 0];\n const hasUserRotation = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;\n const finalRotDeg = hasUserRotation\n ? [rot[0] * (180 / Math.PI), rot[1] * (180 / Math.PI), -rot[2] * (180 / Math.PI)]\n : [180, 0, 0];\n entity.setEulerAngles(finalRotDeg[0], finalRotDeg[1], finalRotDeg[2]);\n\n // Start hidden\n entity.enabled = false;\n app.root.addChild(entity);\n preloadedSplats.set(url, entity);\n\n console.log('[SplatSwap] Preload complete:', url);\n resolve();\n });\n\n asset.on('error', (err: any) => {\n console.error('[SplatSwap] Preload error:', err);\n reject(err);\n });\n\n app.assets.add(asset);\n app.assets.load(asset);\n });\n } catch (error) {\n console.error('[SplatSwap] Error preloading:', url, error);\n }\n }\n\n /**\n * Preload the next splat in the sequence\n */\n async function preloadNextSplat(currentIndex: number): Promise<void> {\n if (!additionalSplats || additionalSplats.length === 0) return;\n\n const nextIndex = (currentIndex + 1) % additionalSplats.length;\n const nextSplat = additionalSplats[nextIndex];\n if (nextSplat && nextSplat.url) {\n await preloadSplat(nextSplat.url);\n }\n }\n\n /**\n * Load a splat (switch to it, handling show/hide or dispose)\n */\n async function loadSwapSplat(url: string): Promise<void> {\n if (url === currentSplatUrl) return;\n if (isLoadingSplat) return;\n if (isDestroyed) return;\n\n isLoadingSplat = true;\n console.log('[SplatSwap] Switching to splat:', url);\n\n try {\n // Hide/dispose current splat\n if (currentSplatUrl && preloadedSplats.has(currentSplatUrl)) {\n if (keepMeshesInMemory) {\n const currentEntity = preloadedSplats.get(currentSplatUrl);\n if (currentEntity) hideSplat(currentEntity);\n } else {\n disposeSplat(currentSplatUrl);\n }\n } else if (splatEntity) {\n // Hide the initial splat entity\n splatEntity.enabled = false;\n }\n\n // Show new splat if already preloaded\n if (preloadedSplats.has(url)) {\n showSplat(url);\n } else {\n // Load new splat\n const asset = new pc.Asset('splat-swap-' + Date.now(), 'gsplat', { url });\n\n await new Promise<void>((resolve, reject) => {\n asset.ready(() => {\n if (isDestroyed) {\n reject(new Error('Viewer destroyed'));\n return;\n }\n\n const entity = new pc.Entity('splat-swap');\n entity.addComponent('gsplat', { asset, unified: true });\n\n // Apply scale/position/rotation\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n const invertX = config.invertXScale || false;\n const invertY = config.invertYScale || false;\n const finalScale = {\n x: invertX ? -scale.x : scale.x,\n y: invertY ? -scale.y : scale.y,\n z: (invertX !== invertY) ? -scale.z : scale.z\n };\n entity.setLocalScale(finalScale.x, finalScale.y, finalScale.z);\n\n const pos = config.position || [0, 0, 0];\n entity.setPosition(pos[0], pos[1], -pos[2]);\n\n const rot = config.rotation || [0, 0, 0];\n const hasUserRotation = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;\n const finalRotDeg = hasUserRotation\n ? [rot[0] * (180 / Math.PI), rot[1] * (180 / Math.PI), -rot[2] * (180 / Math.PI)]\n : [180, 0, 0];\n entity.setEulerAngles(finalRotDeg[0], finalRotDeg[1], finalRotDeg[2]);\n\n app.root.addChild(entity);\n preloadedSplats.set(url, entity);\n currentSplatUrl = url;\n\n console.log('[SplatSwap] New splat loaded and shown:', url);\n resolve();\n });\n\n asset.on('error', (err: any) => {\n console.error('[SplatSwap] Load error:', err);\n reject(err);\n });\n\n app.assets.add(asset);\n app.assets.load(asset);\n });\n }\n\n // Background preload next splat\n const swapIndex = additionalSplats.findIndex(s => s.url === url);\n if (swapIndex !== -1) {\n preloadNextSplat(swapIndex);\n }\n\n } catch (error) {\n console.error('[SplatSwap] Error switching splat:', error);\n } finally {\n isLoadingSplat = false;\n initialSplatLoadDone = true;\n }\n }\n\n /**\n * Get the primary splat URL (first URL tried in loadSplat)\n */\n function getPrimarySplatUrl(): string {\n if (config.sogUrl) return config.sogUrl;\n if (config.splatUrl) return config.splatUrl;\n if (config.fallbackUrls && config.fallbackUrls.length > 0) return config.fallbackUrls[0];\n return '';\n }\n\n /**\n * Update splats based on current progress (called from navigation loop)\n */\n function updateSplats(): void {\n if (!additionalSplats || additionalSplats.length === 0) return;\n\n const numWaypoints = config.waypoints?.length || 1;\n const percentage = currentProgress * 100;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n\n // Skip if no meaningful change\n if (Math.abs(percentage - lastSplatCheckProgress) < 0.1 &&\n waypointIndex === lastSplatCheckWaypointIndex) {\n return;\n }\n\n lastSplatCheckProgress = percentage;\n lastSplatCheckWaypointIndex = waypointIndex;\n\n // Find the best matching splat based on progress\n type SplatSwapType = (typeof additionalSplats)[number];\n let bestSplat: SplatSwapType | null = null;\n let bestTrigger = -Infinity;\n\n for (const splat of additionalSplats) {\n if (splat.waypointIndex !== -1) {\n // Trigger by waypoint index\n if (waypointIndex >= splat.waypointIndex && splat.waypointIndex > bestTrigger) {\n bestTrigger = splat.waypointIndex;\n bestSplat = splat;\n }\n } else if (splat.percentage !== -1) {\n // Trigger by percentage\n if (percentage >= splat.percentage && splat.percentage > bestTrigger) {\n bestTrigger = splat.percentage;\n bestSplat = splat;\n }\n }\n }\n\n // Determine target URL\n const primaryUrl = getPrimarySplatUrl();\n const targetUrl = bestSplat ? bestSplat.url : primaryUrl;\n\n // Switch splat if needed\n if (targetUrl && targetUrl !== currentSplatUrl) {\n if (targetUrl === primaryUrl && splatEntity && !currentSplatUrl) {\n // First load - just mark current URL\n currentSplatUrl = primaryUrl;\n } else if (targetUrl === primaryUrl && splatEntity) {\n // Return to primary splat\n // Hide swap splats and show primary\n preloadedSplats.forEach((entity, url) => {\n if (url !== primaryUrl) {\n if (keepMeshesInMemory) {\n hideSplat(entity);\n } else {\n disposeSplat(url);\n }\n }\n });\n if (splatEntity) splatEntity.enabled = true;\n currentSplatUrl = primaryUrl;\n console.log('[SplatSwap] Returned to primary splat');\n } else {\n // Switch to a different splat\n loadSwapSplat(targetUrl);\n }\n\n // Apply per-splat skybox if specified\n if (bestSplat && bestSplat.skyboxUrl) {\n applySkybox(bestSplat.skyboxUrl, bestSplat.skyboxRotation || 0);\n }\n }\n }\n\n /**\n * Apply a skybox (for per-splat skyboxes)\n * Note: For proper cubemap skyboxes, use 6-face URLs or HDR/EXR environment maps\n * This simplified version loads a single texture and creates a basic skybox\n */\n function applySkybox(url: string, rotation: number = 0): void {\n console.log('[SplatSwap] Applying skybox:', url, 'rotation:', rotation);\n\n // Load skybox as a cubemap texture\n const skyboxAsset = new pc.Asset('skybox-swap-' + Date.now(), 'cubemap', {\n url: url\n }, {\n // Try to load as RGBM for HDR support\n type: pc.TEXTURETYPE_RGBM,\n mipmaps: true\n });\n\n skyboxAsset.ready(() => {\n if (isDestroyed) return;\n\n try {\n // Set as scene skybox - cast to Texture to satisfy TypeScript\n app.scene.skybox = skyboxAsset.resource as pc.Texture;\n app.scene.skyboxMip = 0; // Use highest quality mip\n\n // Apply rotation via quaternion (convert radians to Euler then to Quat)\n if (rotation !== 0) {\n const rotQuat = new pc.Quat();\n rotQuat.setFromEulerAngles(0, rotation * (180 / Math.PI), 0);\n app.scene.skyboxRotation = rotQuat;\n }\n\n console.log('[SplatSwap] Skybox applied successfully');\n } catch (error) {\n console.error('[SplatSwap] Error applying skybox:', error);\n }\n });\n\n skyboxAsset.on('error', (err: any) => {\n console.error('[SplatSwap] Skybox load error:', err);\n });\n\n app.assets.add(skyboxAsset);\n app.assets.load(skyboxAsset);\n }\n\n // =========================================================\n // END SPLATSWAP SYSTEM\n // =========================================================\n\n // Progress-based navigation (matching HTML export)\n let currentProgress = 0; // 0 to 1\n const totalDuration = config.waypoints?.reduce((sum, wp) => sum + (wp.duration || 2000), 0) || 1;\n // Use config.autoplaySpeed if provided, otherwise calculate from total duration\n const playbackSpeed = config.autoplaySpeed !== undefined ? config.autoplaySpeed : 1000 / totalDuration;\n // Normalize loopMode: handle boolean values (true='loop', false='none') and string values\n // Scene data may have boolean loopMode from older versions\n const rawLoopMode = config.loopMode as unknown;\n let loopMode: 'loop' | 'pingpong' | 'none';\n if (rawLoopMode === true) {\n loopMode = 'loop';\n } else if (rawLoopMode === false) {\n loopMode = 'none';\n } else if (rawLoopMode === 'loop' || rawLoopMode === 'pingpong' || rawLoopMode === 'none') {\n loopMode = rawLoopMode;\n } else {\n loopMode = 'loop'; // Default to 'loop' for backward compatibility\n }\n let playbackDirection = 1; // 1 = forward, -1 = backward (for pingpong mode)\n\n console.log('[StorySplat Viewer] Playback config:', {\n loopMode,\n playbackSpeed,\n totalDuration,\n autoPlay: config.autoPlay,\n rawLoopMode: config.loopMode\n });\n\n // Damped camera interpolation (matching Babylon's pullStrength behavior)\n let targetCameraPosition: pc.Vec3 | null = null;\n let targetCameraRotation: pc.Quat | null = null;\n const PULL_STRENGTH = 0.1; // Match Babylon's pullStrength = 0.1\n\n // Elastic user rotation (allows user to look around in tour mode, pulls back to target)\n // Using Euler angles (yaw/pitch) instead of quaternion accumulation to prevent roll drift\n let userYawOffset = 0; // Accumulated yaw offset in degrees\n let userPitchOffset = 0; // Accumulated pitch offset in degrees\n const MAX_PITCH_OFFSET = 60; // Limit pitch to prevent flipping\n let isUserDragging = false;\n let lastPointerX = 0;\n let lastPointerY = 0;\n const USER_ROTATION_SENSITIVITY = 0.3; // How responsive rotation is to drag\n const USER_ROTATION_DECAY = 0.95; // How fast rotation returns to target (0.95 = slow, 0.8 = fast)\n\n // Pre-calculate waypoint positions, rotations, and FOVs for interpolation\n const waypointPositions: pc.Vec3[] = [];\n const waypointRotations: pc.Quat[] = [];\n const waypointFOVs: number[] = [];\n const defaultFOV = config.fov || 60;\n let targetFOV = defaultFOV;\n\n if (config.waypoints && config.waypoints.length > 0) {\n config.waypoints.forEach(wp => {\n const pos = wp.position\n ? new pc.Vec3(wp.position.x, wp.position.y, -wp.position.z)\n : new pc.Vec3(0, config.playerHeight || 1.6, 0);\n waypointPositions.push(pos);\n\n const rot = wp.rotation\n ? convertWaypointRotation(wp.rotation)\n : new pc.Quat();\n waypointRotations.push(rot);\n\n // Extract FOV from waypoint (convert from radians if needed)\n let fov = wp.fov || defaultFOV;\n if (fov < 3.5) {\n // FOV is in radians, convert to degrees\n fov = fov * (180 / Math.PI);\n }\n waypointFOVs.push(fov);\n });\n\n // Initialize target position/rotation/FOV immediately for damping to work on load\n if (waypointPositions.length > 0) {\n targetCameraPosition = waypointPositions[0].clone();\n targetCameraRotation = waypointRotations[0].clone();\n targetFOV = waypointFOVs[0];\n }\n }\n\n // Update camera based on progress (sets targets for damped interpolation)\n function updateCameraFromProgress(progress: number): void {\n if (!waypointControlEnabled || waypointPositions.length < 2) return;\n\n progress = Math.max(0, Math.min(1, progress));\n const numWaypoints = waypointPositions.length;\n\n // Calculate which segment we're in\n const segmentProgress = progress * (numWaypoints - 1);\n const segmentIndex = Math.min(Math.floor(segmentProgress), numWaypoints - 2);\n const t = segmentProgress - segmentIndex;\n\n // Calculate target position (for damped interpolation)\n const startPos = waypointPositions[segmentIndex];\n const endPos = waypointPositions[segmentIndex + 1];\n targetCameraPosition = new pc.Vec3(\n lerp(startPos.x, endPos.x, t),\n lerp(startPos.y, endPos.y, t),\n lerp(startPos.z, endPos.z, t)\n );\n\n // Calculate target rotation (for damped interpolation)\n const startRot = waypointRotations[segmentIndex];\n const endRot = waypointRotations[segmentIndex + 1];\n targetCameraRotation = new pc.Quat();\n targetCameraRotation.slerp(startRot, endRot, t);\n\n // Calculate target FOV (for damped interpolation)\n const startFOV = waypointFOVs[segmentIndex];\n const endFOV = waypointFOVs[segmentIndex + 1];\n targetFOV = lerp(startFOV, endFOV, t);\n\n // Update waypoint index and emit event if changed\n const newIndex = Math.round(progress * (numWaypoints - 1));\n if (newIndex !== currentWaypointIndex) {\n const prevIndex = currentWaypointIndex;\n currentWaypointIndex = newIndex;\n\n // Get current waypoint's camera mode\n const currentWaypoint = config.waypoints![newIndex];\n const waypointCameraMode = currentWaypoint.cameraMode || 'first-person';\n\n // Update orbit mode state based on per-waypoint camera mode\n if (waypointCameraMode === 'orbit') {\n // Enable orbit-style controls while on tour path\n currentWaypointOrbitEnabled = true;\n // Set orbit target (waypoint position or custom target)\n currentWaypointOrbitTarget = currentWaypoint.orbitTarget\n ? new pc.Vec3(\n currentWaypoint.orbitTarget.x,\n currentWaypoint.orbitTarget.y,\n -(currentWaypoint.orbitTarget.z || 0) // Negate Z for coordinate conversion\n )\n : new pc.Vec3(\n waypointPositions[newIndex].x,\n waypointPositions[newIndex].y,\n waypointPositions[newIndex].z\n );\n } else {\n currentWaypointOrbitEnabled = false;\n currentWaypointOrbitTarget = null;\n }\n\n events.emit('waypointChange', {\n index: newIndex,\n waypoint: currentWaypoint,\n prevIndex,\n cameraMode: waypointCameraMode\n });\n\n // Update waypoint audio (play/stop based on waypoint entry/exit)\n updateWaypointAudio(newIndex, prevIndex);\n }\n\n // Emit progress event for UI (ensure clamped value)\n events.emit('progressUpdate', { progress: Math.max(0, Math.min(1, progress)), index: currentWaypointIndex });\n\n // Update splat swap system based on progress\n updateSplats();\n }\n\n // Apply damped camera interpolation (matching Babylon's pullStrength behavior)\n // Only active in tour mode - explore/walk modes control the camera directly\n function updateCameraDamping(): void {\n if (!targetCameraPosition || !targetCameraRotation) return;\n if (!waypointControlEnabled) return; // Don't interfere with explore/walk mode cameras\n\n // Lerp position toward target (matching Babylon's 0.1 pullStrength)\n const currentPos = camera.getPosition();\n const newPos = new pc.Vec3();\n newPos.lerp(currentPos, targetCameraPosition, PULL_STRENGTH);\n camera.setPosition(newPos.x, newPos.y, newPos.z);\n\n // Lerp FOV toward target for smooth transitions between waypoints\n const cameraComponent = camera.camera;\n if (cameraComponent && waypointFOVs.length > 0) {\n const currentFOV = cameraComponent.fov;\n const newFOV = lerp(currentFOV, targetFOV, PULL_STRENGTH);\n cameraComponent.fov = newFOV;\n }\n\n // Decay user rotation offset when not dragging (elastic pull back to target)\n if (!isUserDragging) {\n userYawOffset *= USER_ROTATION_DECAY;\n userPitchOffset *= USER_ROTATION_DECAY;\n // Snap to zero if very small to avoid floating point drift\n if (Math.abs(userYawOffset) < 0.01) userYawOffset = 0;\n if (Math.abs(userPitchOffset) < 0.01) userPitchOffset = 0;\n }\n\n // Build user rotation quaternion from Euler angles (no roll)\n const userRotationOffset = new pc.Quat();\n userRotationOffset.setFromEulerAngles(userPitchOffset, userYawOffset, 0);\n\n // Combine target rotation with user offset for final rotation target\n const targetWithUserOffset = new pc.Quat();\n targetWithUserOffset.mul2(targetCameraRotation, userRotationOffset);\n\n // Slerp rotation toward combined target\n const currentRot = camera.getRotation();\n const newRot = new pc.Quat();\n newRot.slerp(currentRot, targetWithUserOffset, PULL_STRENGTH);\n camera.setRotation(newRot);\n }\n\n // Register camera damping in the render loop\n app.on('update', updateCameraDamping);\n\n // Set progress directly (for scroll)\n function setProgress(progress: number, animate = false): void {\n if (animate) {\n animateToProgress(progress);\n } else {\n currentProgress = Math.max(0, Math.min(1, progress));\n updateCameraFromProgress(currentProgress);\n }\n }\n\n // Animate to a target progress\n function animateToProgress(targetProgress: number, duration = 500): void {\n const startProgress = currentProgress;\n const startTime = performance.now();\n\n const animate = () => {\n const elapsed = performance.now() - startTime;\n const t = Math.min(elapsed / duration, 1);\n const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n\n currentProgress = startProgress + (targetProgress - startProgress) * eased;\n updateCameraFromProgress(currentProgress);\n\n if (t < 1) {\n requestAnimationFrame(animate);\n }\n };\n\n requestAnimationFrame(animate);\n }\n\n // Navigation functions\n function goToWaypoint(index: number): void {\n if (!config.waypoints || index < 0 || index >= config.waypoints.length) return;\n if (!waypointControlEnabled) return;\n\n const targetProgress = index / Math.max(1, config.waypoints.length - 1);\n animateToProgress(targetProgress);\n }\n\n function nextWaypoint(): void {\n if (!config.waypoints || config.waypoints.length === 0) return;\n const next = Math.min(currentWaypointIndex + 1, config.waypoints.length - 1);\n goToWaypoint(next);\n }\n\n function prevWaypoint(): void {\n if (!config.waypoints || config.waypoints.length === 0) return;\n const prev = Math.max(currentWaypointIndex - 1, 0);\n goToWaypoint(prev);\n }\n\n // Continuous playback using requestAnimationFrame\n let lastPlaybackTime = 0;\n let playbackAnimationId: number | null = null;\n\n function playbackLoop(timestamp: number): void {\n if (!isPlaying) return;\n\n if (lastPlaybackTime === 0) lastPlaybackTime = timestamp;\n const deltaTime = (timestamp - lastPlaybackTime) / 1000;\n lastPlaybackTime = timestamp;\n\n // Increment/decrement progress based on direction\n currentProgress += playbackSpeed * deltaTime * playbackDirection;\n\n // Handle end of tour based on loop mode\n if (currentProgress >= 1) {\n console.log('[StorySplat Viewer] End of tour reached, loopMode:', loopMode);\n switch (loopMode) {\n case 'loop':\n console.log('[StorySplat Viewer] Looping - restarting at beginning');\n currentProgress = 0; // Restart at beginning\n break;\n case 'pingpong':\n console.log('[StorySplat Viewer] Pingpong - reversing direction');\n currentProgress = 1;\n playbackDirection = -1; // Reverse direction\n break;\n case 'none':\n console.log('[StorySplat Viewer] No loop - stopping playback');\n currentProgress = 1;\n pause(); // Stop playback\n events.emit('playbackComplete');\n return;\n default:\n console.log('[StorySplat Viewer] Unknown loopMode, stopping:', loopMode);\n currentProgress = 1;\n pause(); // Stop playback\n events.emit('playbackComplete');\n return;\n }\n } else if (currentProgress <= 0) {\n // Only relevant for pingpong mode going backward\n switch (loopMode) {\n case 'pingpong':\n console.log('[StorySplat Viewer] Pingpong - reversing to forward');\n currentProgress = 0;\n playbackDirection = 1; // Reverse back to forward\n break;\n default:\n currentProgress = 0;\n break;\n }\n }\n\n updateCameraFromProgress(currentProgress);\n playbackAnimationId = requestAnimationFrame(playbackLoop);\n }\n\n function play(): void {\n if (isPlaying || !config.waypoints || config.waypoints.length < 2) return;\n isPlaying = true;\n lastPlaybackTime = 0;\n playbackDirection = 1; // Always start going forward\n events.emit('playbackStart');\n playbackAnimationId = requestAnimationFrame(playbackLoop);\n }\n\n function pause(): void {\n isPlaying = false;\n if (playbackAnimationId) {\n cancelAnimationFrame(playbackAnimationId);\n playbackAnimationId = null;\n }\n events.emit('playbackStop');\n }\n\n function stop(): void {\n pause();\n setProgress(0);\n }\n\n // Scroll wheel support\n const scrollCanvas = app.graphicsDevice.canvas as HTMLCanvasElement;\n scrollCanvas.addEventListener('wheel', (e: WheelEvent) => {\n if (!waypointControlEnabled) return;\n e.preventDefault();\n\n // Adjust progress based on scroll (reduced sensitivity)\n const scrollAmount = e.deltaY > 0 ? 0.005 : -0.005;\n const newProgress = Math.max(0, Math.min(1, currentProgress + scrollAmount));\n setProgress(newProgress);\n }, { passive: false });\n\n // Elastic user rotation - track pointer drag for look-around in tour mode\n // Use capture phase to get events before PlayCanvas input system\n scrollCanvas.addEventListener('pointerdown', (e: PointerEvent) => {\n if (!waypointControlEnabled) return; // Only in tour/waypoint mode\n isUserDragging = true;\n lastPointerX = e.clientX;\n lastPointerY = e.clientY;\n }, { capture: true });\n\n scrollCanvas.addEventListener('pointermove', (e: PointerEvent) => {\n if (!waypointControlEnabled || !isUserDragging) return;\n\n const deltaX = e.clientX - lastPointerX;\n const deltaY = e.clientY - lastPointerY;\n lastPointerX = e.clientX;\n lastPointerY = e.clientY;\n\n // Convert drag delta to rotation offset in degrees (yaw and pitch only, no roll)\n const yawDelta = -deltaX * USER_ROTATION_SENSITIVITY;\n const pitchDelta = -deltaY * USER_ROTATION_SENSITIVITY;\n\n // Accumulate yaw and pitch offsets (Euler angles prevent roll drift)\n userYawOffset += yawDelta;\n userPitchOffset += pitchDelta;\n\n // Clamp pitch to prevent camera flipping\n userPitchOffset = Math.max(-MAX_PITCH_OFFSET, Math.min(MAX_PITCH_OFFSET, userPitchOffset));\n }, { capture: true });\n\n scrollCanvas.addEventListener('pointerup', () => {\n isUserDragging = false;\n }, { capture: true });\n\n scrollCanvas.addEventListener('pointerleave', () => {\n isUserDragging = false;\n }, { capture: true });\n\n // =====================================================\n // HOTSPOT SYSTEM\n // =====================================================\n const hotspotEntities: pc.Entity[] = [];\n\n // =====================================================\n // PORTAL SYSTEM (Scene-to-Scene Navigation)\n // =====================================================\n const portalEntities: pc.Entity[] = [];\n\n // Parse hex color to PlayCanvas Color\n function parseColor(hex: string): pc.Color {\n const r = parseInt(hex.slice(1, 3), 16) / 255;\n const g = parseInt(hex.slice(3, 5), 16) / 255;\n const b = parseInt(hex.slice(5, 7), 16) / 255;\n return new pc.Color(r, g, b);\n }\n\n // =====================================================\n // SPATIAL AUDIO SYSTEM\n // =====================================================\n interface HotspotAudioElements {\n audio: HTMLAudioElement;\n audioCtx?: AudioContext;\n source?: MediaElementAudioSourceNode;\n panner?: PannerNode;\n updateAudioPosition?: () => void;\n }\n\n interface VideoSpatialAudio {\n audioCtx: AudioContext;\n source: MediaElementAudioSourceNode;\n panner: PannerNode;\n }\n\n interface WaypointAudioData {\n entity: pc.Entity;\n waypointIndex: number;\n config: any;\n slotId: string;\n playing: boolean;\n autoplayTriggered: boolean;\n assetReady: boolean;\n }\n\n // Track all audio for muting\n const allAudioContexts: AudioContext[] = [];\n const allAudioElements: HTMLAudioElement[] = [];\n let globalMuted = false;\n const storedVolumes = new Map<HTMLAudioElement, number>();\n\n // Waypoint audio tracking\n const waypointAudioMap = new Map<string, WaypointAudioData>();\n\n // =====================================================\n // PARTICLE SYSTEM\n // =====================================================\n const particleEntities: Map<string, pc.Entity> = new Map();\n const particleTextures: Map<string, pc.Texture> = new Map();\n\n // Particle texture URLs (matching HTML export particleSystem.ts)\n const PARTICLE_TEXTURE_URLS: Record<string, string> = {\n flare: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fflare.png?alt=media&token=ce114781-2ac3-41b2-b9c2-34bda0f6eb13',\n circle: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fcircle.png?alt=media&token=fd01b475-2b94-4c24-bc83-2a907045715f',\n spark: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fsparkle.png?alt=media&token=466739a4-ccd7-4295-88c2-dedd681a34f0',\n rain: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Frain.png?alt=media&token=13478487-6259-4906-838c-ad592db893e4',\n smoke: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fsmoke.png?alt=media&token=6cece4f8-87cf-47f9-974d-c4dd6a649f0f',\n };\n\n /**\n * Load a particle texture (with caching)\n */\n function loadParticleTexture(name: string, url: string): Promise<pc.Texture> {\n return new Promise((resolve, reject) => {\n // Check cache first\n if (particleTextures.has(name)) {\n resolve(particleTextures.get(name)!);\n return;\n }\n\n const asset = new pc.Asset(name, 'texture', { url: url });\n asset.on('load', () => {\n // Check if viewer was destroyed while loading\n if (isDestroyed) {\n console.log(`[Particle] Ignoring texture load - viewer was destroyed: ${name}`);\n reject(new Error('Viewer destroyed'));\n return;\n }\n const texture = asset.resource as pc.Texture;\n particleTextures.set(name, texture);\n resolve(texture);\n });\n asset.on('error', (err: any) => {\n console.error(`[Particle] Failed to load texture: ${name}`, err);\n reject(err);\n });\n app.assets.add(asset);\n app.assets.load(asset);\n });\n }\n\n /**\n * Create a particle system entity (matching HTML export createParticleSystem)\n * Enhanced with full feature parity: direction1/direction2, box emitter positioning,\n * scaleX/Y, colorDead, angular speed range, initial rotation range\n */\n function createParticleSystemEntity(psConfig: any): pc.Entity {\n const entity = new pc.Entity(psConfig.name || 'particle-system');\n\n // Helper to safely get position coordinates\n const getPos = (vec: any, defaultVal = 0) => ({\n x: vec?._x ?? vec?.x ?? defaultVal,\n y: vec?._y ?? vec?.y ?? defaultVal,\n z: vec?._z ?? vec?.z ?? defaultVal\n });\n\n // Helper to safely get color\n const getColor = (color: any) => color || { r: 1, g: 1, b: 1, a: 1 };\n\n const emitterPos = getPos(psConfig.emitterPosition, 0);\n const gravity = getPos(psConfig.gravity, 0);\n const color1 = getColor(psConfig.color1);\n const color2 = getColor(psConfig.color2);\n const colorDead = getColor(psConfig.colorDead);\n\n // Get direction1/direction2 for directional emission (matching HTML export)\n const direction1 = getPos(psConfig.direction1, 0);\n const direction2 = getPos(psConfig.direction2, 0);\n\n // Calculate lifetime from min/max\n const minLifeTime = psConfig.minLifeTime ?? 1.0;\n const maxLifeTime = psConfig.maxLifeTime ?? 3.0;\n const lifetime = (minLifeTime + maxLifeTime) / 2;\n\n // Determine emitter shape and calculate extents\n let emitterShape: number = pc.EMITTERSHAPE_BOX;\n let emitterExtents = new pc.Vec3(0.1, 0.1, 0.1);\n let emitterOffset = new pc.Vec3(0, 0, 0);\n\n if (psConfig.emitterType === 'sphere' || psConfig.emitterShape === 'sphere') {\n emitterShape = pc.EMITTERSHAPE_SPHERE;\n const radius = psConfig.emitterRadius || 0.1;\n emitterExtents = new pc.Vec3(radius, radius, radius);\n } else if (psConfig.emitterShape === 'box' && psConfig.emitBoxMin && psConfig.emitBoxMax) {\n // Box emitter with min/max emit box (matching HTML export)\n emitterShape = pc.EMITTERSHAPE_BOX;\n const boxMin = getPos(psConfig.emitBoxMin, 0);\n const boxMax = getPos(psConfig.emitBoxMax, 0);\n\n // Calculate extents as half the size of the box\n emitterExtents = new pc.Vec3(\n Math.abs(boxMax.x - boxMin.x) / 2,\n Math.abs(boxMax.y - boxMin.y) / 2,\n Math.abs(boxMax.z - boxMin.z) / 2\n );\n\n // Calculate center offset from emitter position\n emitterOffset = new pc.Vec3(\n (boxMin.x + boxMax.x) / 2,\n (boxMin.y + boxMax.y) / 2,\n -(boxMin.z + boxMax.z) / 2 // Negate Z for coordinate conversion\n );\n } else if (psConfig.emitterShape === 'point') {\n // Point emitter - use very small extents\n emitterShape = pc.EMITTERSHAPE_BOX;\n emitterExtents = new pc.Vec3(0.01, 0.01, 0.01);\n } else {\n // Default box emitter\n emitterExtents = new pc.Vec3(\n psConfig.emitterExtents?.x || psConfig.emitterRadius || 0.1,\n psConfig.emitterExtents?.y || psConfig.emitterRadius || 0.1,\n psConfig.emitterExtents?.z || psConfig.emitterRadius || 0.1\n );\n }\n\n // Determine blend mode (matching HTML export blendModeMap)\n let blendMode: number = pc.BLEND_ADDITIVEALPHA;\n const blendModeStr = psConfig.blendMode || '';\n if (blendModeStr === 'BLENDMODE_STANDARD' || blendModeStr === 'normal' || blendModeStr === 'alpha') {\n blendMode = pc.BLEND_NORMAL;\n } else if (blendModeStr === 'BLENDMODE_MULTIPLY' || blendModeStr === 'multiply') {\n blendMode = pc.BLEND_MULTIPLICATIVE;\n } else if (blendModeStr === 'BLENDMODE_ONEONE') {\n blendMode = pc.BLEND_ADDITIVE;\n } else if (blendModeStr === 'BLENDMODE_ADD' || blendModeStr === 'BLENDMODE_MULTIPLYADD') {\n blendMode = pc.BLEND_ADDITIVEALPHA;\n }\n\n // Build world velocity graph with gravity\n const gravityX = gravity.x || 0;\n const gravityY = gravity.y || 0; // Don't default to -9.8, use config value\n const gravityZ = -(gravity.z || 0); // Negate Z for coordinate conversion\n\n // Calculate accumulated velocity at end of lifetime due to gravity\n const endVelX = gravityX * lifetime;\n const endVelY = gravityY * lifetime;\n const endVelZ = gravityZ * lifetime;\n\n // Get angular speed range (matching HTML export minAngularSpeed/maxAngularSpeed)\n const minAngularSpeed = psConfig.minAngularSpeed ?? 0;\n const maxAngularSpeed = psConfig.maxAngularSpeed ?? (psConfig.angularSpeed ?? 0);\n\n // Get initial rotation range (matching HTML export minInitialRotation/maxInitialRotation)\n const minInitialRotation = psConfig.minInitialRotation ?? 0;\n const maxInitialRotation = psConfig.maxInitialRotation ?? 0;\n\n // Get scale ranges (matching HTML export minScaleX/maxScaleX/minScaleY/maxScaleY)\n const minSize = psConfig.minSize ?? 0.1;\n const maxSize = psConfig.maxSize ?? 0.3;\n const minScaleX = psConfig.minScaleX ?? 1;\n const maxScaleX = psConfig.maxScaleX ?? 1;\n const minScaleY = psConfig.minScaleY ?? 1;\n const maxScaleY = psConfig.maxScaleY ?? 1;\n\n // Calculate emit power range\n const minEmitPower = psConfig.minEmitPower ?? 1;\n const maxEmitPower = psConfig.maxEmitPower ?? 2;\n\n // Calculate direction-based velocity (average of direction1 and direction2)\n const avgDirX = (direction1.x + direction2.x) / 2;\n const avgDirY = (direction1.y + direction2.y) / 2;\n const avgDirZ = -(direction1.z + direction2.z) / 2; // Negate Z\n\n // Create particle system with enhanced parameters (full feature parity)\n entity.addComponent('particlesystem', {\n numParticles: psConfig.numParticles || 500,\n lifetime: lifetime,\n rate: psConfig.emitRate || 50,\n emitterShape: emitterShape,\n emitterExtents: emitterExtents,\n emitterRadius: psConfig.emitterRadius || 0.1,\n\n // Initial rotation range (matching HTML export)\n startAngle: minInitialRotation,\n startAngle2: maxInitialRotation !== 0 ? maxInitialRotation : 360,\n\n // Speed (radial velocity)\n radialSpeedGraph: new pc.Curve([0, minEmitPower, 1, maxEmitPower]),\n\n // Local space velocity (for directional emission using direction1/direction2)\n localVelocityGraph: new pc.CurveSet([\n [0, avgDirX * minEmitPower],\n [0, avgDirY * minEmitPower],\n [0, avgDirZ * minEmitPower]\n ]),\n localVelocityGraph2: new pc.CurveSet([\n [0, avgDirX * maxEmitPower],\n [0, avgDirY * maxEmitPower],\n [0, avgDirZ * maxEmitPower]\n ]),\n\n // World velocity (gravity effect over lifetime)\n velocityGraph: new pc.CurveSet([\n [0, 0, 1, endVelX],\n [0, 0, 1, endVelY],\n [0, 0, 1, endVelZ]\n ]),\n\n // Scale over lifetime with X/Y scale support\n scaleGraph: new pc.Curve([0, minSize * minScaleX, 1, maxSize * maxScaleX]),\n scaleGraph2: new pc.Curve([0, minSize * minScaleY, 1, maxSize * maxScaleY]),\n\n // Rotation speed range over lifetime (matching HTML export minAngularSpeed/maxAngularSpeed)\n rotationSpeedGraph: new pc.Curve([0, minAngularSpeed]),\n rotationSpeedGraph2: maxAngularSpeed !== minAngularSpeed\n ? new pc.Curve([0, maxAngularSpeed])\n : undefined,\n\n // Color over lifetime with colorDead (RGB) - start -> middle -> dead\n colorGraph: new pc.CurveSet([\n [0, color1.r, 0.5, color2.r, 1, colorDead.r],\n [0, color1.g, 0.5, color2.g, 1, colorDead.g],\n [0, color1.b, 0.5, color2.b, 1, colorDead.b]\n ]),\n\n // Alpha over lifetime with colorDead alpha\n alphaGraph: new pc.Curve([\n 0, color1.a ?? 1,\n 0.5, color2.a ?? 0.8,\n 1, colorDead.a ?? 0\n ]),\n\n // Rendering settings\n blend: blendMode,\n depthWrite: psConfig.depthWrite ?? false,\n depthSoftening: psConfig.softParticles ?? 0,\n lighting: psConfig.lighting ?? false,\n halfLambert: psConfig.halfLambert ?? false,\n alignToMotion: psConfig.alignToMotion ?? false,\n stretch: psConfig.stretch || 0,\n preWarm: psConfig.preWarm ?? false,\n loop: psConfig.loop ?? true,\n autoPlay: psConfig.autoPlay ?? true,\n\n // Sorting and orientation\n sort: psConfig.sort ?? 0,\n orientation: psConfig.orientation ?? 0\n });\n\n // Set local space mode\n if (entity.particlesystem) {\n entity.particlesystem.localSpace = psConfig.localSpace ?? false;\n }\n\n // Set position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n // Add emitter offset for box emitter positioning\n entity.setPosition(\n emitterPos.x + emitterOffset.x,\n emitterPos.y + emitterOffset.y,\n -emitterPos.z + emitterOffset.z\n );\n\n console.log(`[Particle] Entity configured at position: (${emitterPos.x + emitterOffset.x}, ${emitterPos.y + emitterOffset.y}, ${-emitterPos.z + emitterOffset.z})`);\n console.log(`[Particle] Emitter shape: ${psConfig.emitterShape || 'box'}, extents: ${emitterExtents.x}, ${emitterExtents.y}, ${emitterExtents.z}`);\n console.log(`[Particle] Gravity: (${gravityX}, ${gravityY}, ${gravityZ})`);\n console.log(`[Particle] Colors: start=${JSON.stringify(color1)}, mid=${JSON.stringify(color2)}, dead=${JSON.stringify(colorDead)}`);\n console.log(`[Particle] Angular speed: ${minAngularSpeed} - ${maxAngularSpeed}, Initial rotation: ${minInitialRotation} - ${maxInitialRotation}`);\n\n return entity;\n }\n\n /**\n * Initialize all particle systems from config\n */\n async function initParticleSystems(): Promise<void> {\n console.log('═══════════════════════════════════════');\n console.log('🎆 PARTICLE SYSTEM INITIALIZATION');\n console.log('═══════════════════════════════════════');\n console.log('[Particle] config.particles:', config.particles);\n console.log('[Particle] Type:', typeof config.particles);\n console.log('[Particle] Is Array:', Array.isArray(config.particles));\n\n if (!config.particles || config.particles.length === 0) {\n console.log('[Particle] ⚠️ No particle systems to create (particles array is empty or undefined)');\n console.log('═══════════════════════════════════════');\n return;\n }\n\n console.log(`[Particle] 📋 Found ${config.particles.length} particle system(s) to create`);\n\n for (let i = 0; i < config.particles.length; i++) {\n const ps = config.particles[i];\n console.log(`[Particle] --- Particle System ${i + 1}/${config.particles.length} ---`);\n console.log('[Particle] Raw config:', JSON.stringify(ps, null, 2));\n\n try {\n // Get texture URL - support custom textures\n const textureName = ps.particleTexture || 'flare';\n let textureUrl: string;\n let textureCacheKey: string;\n\n if (textureName === 'custom' && ps.customTextureUrl) {\n // Use custom texture URL\n textureUrl = ps.customTextureUrl;\n textureCacheKey = `custom_${ps.id || ps.name || i}`;\n console.log(`[Particle] Custom texture: ${textureUrl.substring(0, 60)}...`);\n } else {\n // Use predefined texture\n textureUrl = PARTICLE_TEXTURE_URLS[textureName] || PARTICLE_TEXTURE_URLS['flare'];\n textureCacheKey = textureName;\n console.log(`[Particle] Texture: ${textureName} -> ${textureUrl.substring(0, 60)}...`);\n }\n\n // Load texture\n console.log('[Particle] Loading texture...');\n const texture = await loadParticleTexture(textureCacheKey, textureUrl);\n console.log('[Particle] ✅ Texture loaded:', texture.name);\n\n // Create particle system entity\n console.log('[Particle] Creating entity...');\n const entity = createParticleSystemEntity(ps);\n console.log('[Particle] ✅ Entity created:', entity.name);\n\n // Set the texture on the particle system\n if (entity.particlesystem) {\n entity.particlesystem.colorMap = texture;\n console.log('[Particle] ✅ Texture applied to particle system');\n console.log('[Particle] Particle system properties:', {\n numParticles: entity.particlesystem.numParticles,\n lifetime: entity.particlesystem.lifetime,\n rate: entity.particlesystem.rate,\n loop: entity.particlesystem.loop,\n autoPlay: entity.particlesystem.autoPlay\n });\n } else {\n console.warn('[Particle] ⚠️ Entity has no particlesystem component!');\n }\n\n // Add to scene\n app.root.addChild(entity);\n console.log('[Particle] ✅ Entity added to scene');\n\n // Log entity position\n const pos = entity.getPosition();\n console.log(`[Particle] Position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`);\n\n // Store reference\n const safeName = (ps.id || ps.name || `particle-${particleEntities.size}`).replace(/[^a-zA-Z0-9]/g, '_');\n particleEntities.set(safeName, entity);\n\n console.log(`[Particle] ✅ SUCCESS: Created \"${ps.name || safeName}\"`);\n } catch (err: any) {\n console.error(`[Particle] ❌ FAILED to create particle system: ${ps.name}`);\n console.error(`[Particle] Error message: ${err?.message || 'No message'}`);\n console.error(`[Particle] Error stack: ${err?.stack || 'No stack'}`);\n console.error(`[Particle] Error object:`, err);\n }\n }\n\n console.log('═══════════════════════════════════════');\n console.log(`[Particle] 🎉 COMPLETE: ${particleEntities.size}/${config.particles.length} particle systems created`);\n console.log('[Particle] Active particle systems:', Array.from(particleEntities.keys()));\n console.log('═══════════════════════════════════════');\n }\n\n /**\n * Stop all particle systems\n */\n function stopAllParticles(): void {\n particleEntities.forEach((entity) => {\n if (entity.particlesystem) {\n entity.particlesystem.stop();\n }\n });\n }\n\n /**\n * Start all particle systems\n */\n function startAllParticles(): void {\n particleEntities.forEach((entity) => {\n if (entity.particlesystem) {\n entity.particlesystem.play();\n }\n });\n }\n\n /**\n * Toggle a specific particle system by ID\n */\n function toggleParticle(id: string): void {\n const entity = particleEntities.get(id);\n if (entity) {\n entity.enabled = !entity.enabled;\n }\n }\n\n // =====================================================\n // CUSTOM MESH SYSTEM (3D Models - GLB/GLTF)\n // =====================================================\n interface CustomMeshData {\n entity: pc.Entity;\n config: any;\n animComponent?: any;\n audioSlotId?: string;\n isAnimPlaying: boolean;\n audioPlaying: boolean;\n }\n\n const customMeshEntities: Map<string, CustomMeshData> = new Map();\n\n /**\n * Helper to play an animation component using various methods\n * PlayCanvas has different ways to start animations depending on the setup\n */\n function playAnimComponent(anim: any): void {\n try {\n // Method 1: Set playing property (simple animations)\n anim.playing = true;\n\n // Method 2: Use baseLayer.play() if available (Anim component)\n if (anim.baseLayer && typeof anim.baseLayer.play === 'function') {\n // Get the first available state/clip name\n const states = anim.baseLayer.states;\n if (states && states.length > 0) {\n const firstState = states[0];\n if (firstState && firstState !== 'START' && firstState !== 'END' && firstState !== 'ANY') {\n anim.baseLayer.play(firstState);\n console.log('[CustomMesh] Playing animation state:', firstState);\n }\n }\n }\n\n // Method 3: Direct play() method\n if (typeof anim.play === 'function') {\n anim.play();\n }\n\n // Method 4: Set speed to ensure animation runs\n if (anim.speed !== undefined) {\n anim.speed = 1;\n }\n\n console.log('[CustomMesh] Animation started, playing:', anim.playing);\n } catch (err) {\n console.error('[CustomMesh] Error playing animation:', err);\n }\n }\n\n /**\n * Helper to pause an animation component\n */\n function pauseAnimComponent(anim: any): void {\n try {\n anim.playing = false;\n if (anim.speed !== undefined) {\n anim.speed = 0;\n }\n if (typeof anim.pause === 'function') {\n anim.pause();\n }\n } catch (err) {\n console.error('[CustomMesh] Error pausing animation:', err);\n }\n }\n\n /**\n * Sample an animation track at a specific time\n * Handles keyframe interpolation for position, rotation, and scale\n */\n function sampleAnimationTrack(track: any, time: number): any {\n try {\n const keys = track.keys || track.keyframes || [];\n if (keys.length === 0) return null;\n\n // Find surrounding keyframes\n let prevKey = keys[0];\n let nextKey = keys[keys.length - 1];\n\n for (let i = 0; i < keys.length - 1; i++) {\n const currentTime = keys[i].time ?? keys[i][0];\n const nextTime = keys[i + 1].time ?? keys[i + 1][0];\n if (time >= currentTime && time < nextTime) {\n prevKey = keys[i];\n nextKey = keys[i + 1];\n break;\n }\n }\n\n // Calculate interpolation factor\n const prevTime = prevKey.time ?? prevKey[0] ?? 0;\n const nextTime = nextKey.time ?? nextKey[0] ?? 0;\n const duration = nextTime - prevTime;\n const t = duration > 0 ? (time - prevTime) / duration : 0;\n\n // Get values (handle both object and array formats)\n const prevValue = prevKey.value ?? prevKey.slice?.(1) ?? prevKey;\n const nextValue = nextKey.value ?? nextKey.slice?.(1) ?? nextKey;\n\n // Interpolate based on track type\n const trackType = track.type || track.property || 'position';\n\n if (trackType.includes('rotation') || trackType.includes('quaternion')) {\n // Quaternion SLERP interpolation\n if (Array.isArray(prevValue) && prevValue.length >= 4) {\n return slerpQuat(prevValue, nextValue, t);\n }\n return prevValue;\n } else {\n // Linear interpolation for position/scale\n if (Array.isArray(prevValue)) {\n return prevValue.map((v: number, i: number) => {\n const nv = nextValue[i] ?? v;\n return v + (nv - v) * t;\n });\n }\n return prevValue;\n }\n } catch (e) {\n return null;\n }\n }\n\n /**\n * Simple quaternion SLERP\n */\n function slerpQuat(a: number[], b: number[], t: number): number[] {\n // Calculate cosine of angle\n let cosom = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];\n\n // Adjust signs if necessary\n const bAdj = [...b];\n if (cosom < 0) {\n cosom = -cosom;\n bAdj[0] = -bAdj[0];\n bAdj[1] = -bAdj[1];\n bAdj[2] = -bAdj[2];\n bAdj[3] = -bAdj[3];\n }\n\n // Calculate coefficients\n let scale0: number, scale1: number;\n if (1 - cosom > 0.0001) {\n const omega = Math.acos(cosom);\n const sinom = Math.sin(omega);\n scale0 = Math.sin((1 - t) * omega) / sinom;\n scale1 = Math.sin(t * omega) / sinom;\n } else {\n // Use linear interpolation for very close quaternions\n scale0 = 1 - t;\n scale1 = t;\n }\n\n return [\n scale0 * a[0] + scale1 * bAdj[0],\n scale0 * a[1] + scale1 * bAdj[1],\n scale0 * a[2] + scale1 * bAdj[2],\n scale0 * a[3] + scale1 * bAdj[3]\n ];\n }\n\n /**\n * Apply an animation value to a target entity\n */\n function applyAnimationValue(rootEntity: pc.Entity, targetPath: string, trackType: string, value: any): void {\n try {\n // Parse the target path (e.g., \"Bone001/position\" or just \"position\")\n const parts = targetPath.split('/');\n const property = parts.pop() || targetPath;\n\n // Find the target node\n let target: pc.Entity | null = rootEntity;\n if (parts.length > 0) {\n const nodeName = parts.join('/');\n target = rootEntity.findByName(nodeName) as pc.Entity || rootEntity;\n }\n\n if (!target) return;\n\n // Apply the value based on property type\n const normalizedProp = property.toLowerCase();\n\n if (normalizedProp.includes('position') || normalizedProp.includes('translation')) {\n if (Array.isArray(value) && value.length >= 3) {\n target.setLocalPosition(value[0], value[1], value[2]);\n }\n } else if (normalizedProp.includes('rotation') || normalizedProp.includes('quaternion')) {\n if (Array.isArray(value) && value.length >= 4) {\n target.setLocalRotation(new pc.Quat(value[0], value[1], value[2], value[3]));\n } else if (Array.isArray(value) && value.length >= 3) {\n // Euler angles\n target.setLocalEulerAngles(value[0], value[1], value[2]);\n }\n } else if (normalizedProp.includes('scale')) {\n if (Array.isArray(value) && value.length >= 3) {\n target.setLocalScale(value[0], value[1], value[2]);\n }\n }\n } catch (e) {\n // Silently ignore - animation track may reference non-existent node\n }\n }\n\n /**\n * Enhanced animation player that handles both anim and animation components\n * Also handles the new format with type info including GLB animations\n */\n function playAnimComponentV2(animInfo: any): void {\n const { type, component, node, animations } = animInfo;\n console.log('[CustomMesh] Playing animation, type:', type, 'node:', node?.name, 'animations:', animations?.length);\n\n try {\n if (type === 'pc-anim') {\n // Simple PlayCanvas anim component - matches HTML export approach\n console.log('[CustomMesh] Using simple pc-anim approach (like HTML export)');\n component.playing = true;\n animInfo.isPlaying = true;\n console.log('[CustomMesh] Animation started - playing:', component.playing);\n } else if (type === 'animation') {\n // Legacy Animation component\n component.playing = true;\n component.loop = true;\n if (typeof component.play === 'function') {\n // Play the first animation clip\n const clips = Object.keys(component.animations || {});\n if (clips.length > 0) {\n component.play(clips[0], 1);\n console.log('[CustomMesh] Playing legacy animation clip:', clips[0]);\n } else {\n component.play();\n }\n }\n } else if (type === 'glb-skeleton') {\n // GLB skeleton animation - use PlayCanvas AnimComponent\n console.log('[CustomMesh] Starting GLB skeleton animation playback');\n const { modelEntity, asset, animations: animList } = animInfo;\n\n if (animList && animList.length > 0) {\n const firstAnim = animList[0];\n console.log('[CustomMesh] Animation clip:', firstAnim);\n console.log('[CustomMesh] Animation name:', firstAnim._name || firstAnim.name);\n console.log('[CustomMesh] Animation duration:', firstAnim._duration || firstAnim.duration);\n\n try {\n // Try to add AnimComponent if not exists\n if (!modelEntity.anim) {\n console.log('[CustomMesh] Adding AnimComponent to modelEntity');\n\n // Create simple state graph for the animation\n const stateGraphData = {\n layers: [{\n name: 'Base',\n states: [\n { name: 'START', speed: 1 },\n { name: 'Idle', speed: 1, loop: true }\n ],\n transitions: [{\n from: 'START',\n to: 'Idle',\n time: 0,\n conditions: []\n }]\n }]\n };\n\n modelEntity.addComponent('anim', {\n activate: true,\n speed: 1\n });\n\n // Load the state graph\n if (modelEntity.anim) {\n modelEntity.anim.loadStateGraph(stateGraphData);\n\n // Assign the animation to the Idle state\n const animAsset = firstAnim;\n modelEntity.anim.assignAnimation('Base.Idle', animAsset, 'Base');\n\n console.log('[CustomMesh] AnimComponent added and animation assigned');\n }\n }\n\n // Now play via the anim component\n if (modelEntity.anim) {\n modelEntity.anim.playing = true;\n modelEntity.anim.speed = 1;\n animInfo.animComponent = modelEntity.anim;\n animInfo.isPlaying = true;\n console.log('[CustomMesh] GLB animation playing via AnimComponent');\n } else {\n // Fallback: Manual curve sampling using PlayCanvas AnimTrack format\n console.log('[CustomMesh] Falling back to manual curve sampling');\n animInfo.isPlaying = true;\n animInfo.startTime = Date.now();\n\n const updateHandler = (dt: number) => {\n if (!animInfo.isPlaying) return;\n\n const elapsed = (Date.now() - animInfo.startTime) / 1000;\n const duration = firstAnim._duration || firstAnim.duration || 1;\n const loopedTime = elapsed % duration;\n\n // Sample using PlayCanvas AnimTrack._curves\n try {\n const curves = firstAnim._curves || [];\n curves.forEach((curve: any, idx: number) => {\n if (!curve) return;\n\n // Get the target path from AnimTrack\n const paths = firstAnim._paths?.[idx];\n if (!paths || !paths.entityPath) return;\n\n // Find target entity\n let target = modelEntity.findByPath(paths.entityPath.join('/'));\n if (!target) target = modelEntity.findByName(paths.entityPath[paths.entityPath.length - 1]);\n if (!target) return;\n\n // Sample the curve at current time\n const value = curve.evaluate?.(loopedTime);\n if (value === undefined || value === null) return;\n\n // Apply based on property type\n const prop = paths.component || paths.propertyPath?.[0];\n if (prop === 'localPosition' || prop === 'position') {\n if (Array.isArray(value)) {\n target.setLocalPosition(value[0], value[1], value[2]);\n }\n } else if (prop === 'localRotation' || prop === 'rotation') {\n if (Array.isArray(value) && value.length >= 4) {\n target.setLocalRotation(new pc.Quat(value[0], value[1], value[2], value[3]));\n }\n } else if (prop === 'localScale' || prop === 'scale') {\n if (Array.isArray(value)) {\n target.setLocalScale(value[0], value[1], value[2]);\n }\n }\n });\n } catch (e) {\n // Silently ignore - curve evaluation errors\n }\n };\n\n animInfo.updateHandler = updateHandler;\n app.on('update', updateHandler);\n console.log('[CustomMesh] Manual animation playback started');\n }\n } catch (err) {\n console.error('[CustomMesh] Error setting up GLB animation:', err);\n }\n }\n } else if (type === 'anim') {\n // New Anim component\n component.playing = true;\n component.speed = 1;\n\n // Try to play via baseLayer\n if (component.baseLayer) {\n const states = component.baseLayer.states || [];\n const playableState = states.find((s: string) => s !== 'START' && s !== 'END' && s !== 'ANY');\n if (playableState) {\n component.baseLayer.play(playableState);\n console.log('[CustomMesh] Playing anim state:', playableState);\n }\n }\n }\n\n // Only log component state if component exists (not for glb-skeleton)\n if (component) {\n console.log('[CustomMesh] Animation component state - playing:', component.playing, 'speed:', component.speed);\n }\n } catch (err) {\n console.error('[CustomMesh] Error in playAnimComponentV2:', err);\n }\n }\n\n /**\n * Enhanced animation pauser\n */\n function pauseAnimComponentV2(animInfo: any): void {\n const { type, component } = animInfo;\n\n try {\n if (type === 'pc-anim') {\n // Simple PlayCanvas anim component - matches HTML export approach\n component.playing = false;\n animInfo.isPlaying = false;\n console.log('[CustomMesh] pc-anim animation paused');\n } else if (type === 'glb-skeleton') {\n // Stop GLB skeleton animation\n animInfo.isPlaying = false;\n\n // If we have an AnimComponent, pause it\n if (animInfo.animComponent) {\n animInfo.animComponent.playing = false;\n console.log('[CustomMesh] GLB skeleton animation paused via AnimComponent');\n }\n\n // Remove update handler if using manual playback\n if (animInfo.updateHandler) {\n app.off('update', animInfo.updateHandler);\n animInfo.updateHandler = null;\n console.log('[CustomMesh] GLB manual animation paused');\n }\n } else if (component) {\n component.playing = false;\n component.speed = 0;\n\n if (type === 'animation' && typeof component.pause === 'function') {\n component.pause();\n }\n }\n } catch (err) {\n console.error('[CustomMesh] Error pausing animation:', err);\n }\n }\n\n /**\n * Load and create a custom mesh entity (GLB/GLTF model)\n * Matches HTML export customMeshSystem.ts\n */\n function loadCustomMesh(meshConfig: any, index: number): pc.Entity | null {\n console.log('[CustomMesh] Loading mesh', index, ':', meshConfig.name, meshConfig);\n\n // Validate modelUrl before attempting to load\n if (!meshConfig.modelUrl || typeof meshConfig.modelUrl !== 'string' || meshConfig.modelUrl.trim() === '') {\n console.warn('[CustomMesh] Skipping mesh', meshConfig.name, '- no valid modelUrl provided. Config:', meshConfig);\n return null;\n }\n\n // Create container entity\n const entity = new pc.Entity('custom-mesh-' + index);\n\n // Position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n const pos = meshConfig.position || { x: 0, y: 0, z: 0 };\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n\n // Rotation (convert from radians to degrees)\n if (meshConfig.rotation) {\n const rot = meshConfig.rotation;\n const radToDeg = 180 / Math.PI;\n entity.setEulerAngles(\n (rot._x ?? rot.x ?? 0) * radToDeg,\n (rot._y ?? rot.y ?? 0) * radToDeg,\n (rot._z ?? rot.z ?? 0) * radToDeg\n );\n }\n\n // Scale\n if (meshConfig.scale) {\n const scale = meshConfig.scale;\n entity.setLocalScale(\n scale._x ?? scale.x ?? 1,\n scale._y ?? scale.y ?? 1,\n scale._z ?? scale.z ?? 1\n );\n }\n\n // Load GLB/GLTF model\n const modelUrl = meshConfig.modelUrl.trim();\n console.log('[CustomMesh] Loading model from URL:', modelUrl);\n\n const modelAsset = new pc.Asset(\n 'mesh-model-' + index,\n 'container',\n { url: modelUrl }\n );\n\n app.assets.add(modelAsset);\n\n // Store mesh data reference\n const meshId = meshConfig.id || `mesh-${index}`;\n const meshData: CustomMeshData = {\n entity,\n config: meshConfig,\n isAnimPlaying: false,\n audioPlaying: false\n };\n customMeshEntities.set(meshId, meshData);\n\n modelAsset.ready((asset: any) => {\n try {\n console.log('[CustomMesh] Model loaded:', meshConfig.name);\n\n // Validate asset resource before instantiation\n if (!asset || !asset.resource) {\n console.error('[CustomMesh] Invalid asset resource for mesh:', meshConfig.name);\n return;\n }\n\n // Instantiate the model\n const modelEntity = asset.resource.instantiateRenderEntity();\n if (!modelEntity) {\n console.error('[CustomMesh] Failed to instantiate render entity for mesh:', meshConfig.name);\n return;\n }\n\n entity.addChild(modelEntity);\n\n // Store reference to model entity for animation\n (entity as any).modelEntity = modelEntity;\n\n // IMPORTANT: Recursively enable all children in the hierarchy\n // Some GLB/GLTF files have nodes disabled by default\n function enableAllChildren(node: any): void {\n node.enabled = true;\n if (node.children) {\n node.children.forEach((child: any) => enableAllChildren(child));\n }\n }\n enableAllChildren(modelEntity);\n\n // Count children for debugging\n let childCount = 0;\n modelEntity.forEach(() => { childCount++; });\n console.log('[CustomMesh] Enabled all children for:', meshConfig.name, '- Total nodes:', childCount);\n\n // Apply static opacity if specified (not animated mode)\n if (meshConfig.opacityMode !== 'animated' && meshConfig.opacity !== undefined && meshConfig.opacity < 1) {\n // Apply opacity to all mesh instances\n applyOpacityToCustomMesh(entity, meshConfig.opacity);\n console.log('[CustomMesh] Applied static opacity:', meshConfig.opacity, 'to', meshConfig.name);\n }\n\n // Setup billboarding if enabled (with range support)\n if (meshConfig.billboard) {\n // Store original rotation for when billboard is disabled\n const originalRotation = entity.getEulerAngles().clone();\n\n // Initialize billboard active state\n (entity as any)._billboardActive = !meshConfig.billboardRange; // Active by default if no range\n\n // Create update handler to make mesh face camera when billboard is active\n app.on('update', () => {\n if (!entity.enabled) return;\n\n // Check if billboard should be active (controlled by updateCustomMeshVisibility)\n const billboardActive = (entity as any)._billboardActive;\n if (!billboardActive) {\n // Restore original rotation when billboard is disabled\n entity.setEulerAngles(originalRotation.x, originalRotation.y, originalRotation.z);\n return;\n }\n\n const camPos = camera.getPosition();\n const meshPos = entity.getPosition();\n\n // Calculate angle to camera (Y-axis rotation only for billboard)\n const dx = camPos.x - meshPos.x;\n const dz = camPos.z - meshPos.z;\n const angle = Math.atan2(dx, dz) * (180 / Math.PI);\n\n // Get current rotation and only update Y\n const currentRot = entity.getEulerAngles();\n entity.setEulerAngles(currentRot.x, angle, currentRot.z);\n });\n console.log('[CustomMesh] Billboard enabled for:', meshConfig.name, meshConfig.billboardRange ? '(with range control)' : '(always active)');\n }\n\n // Handle animations - ALWAYS check for GLB animations regardless of interaction settings\n // Log what's available in the asset for debugging\n const hasGlbAnimations = asset.resource?.animations?.length > 0;\n console.log('[CustomMesh] Asset resource for', meshConfig.name, ':', {\n hasAnimations: hasGlbAnimations,\n animationCount: asset.resource?.animations?.length || 0,\n animations: asset.resource?.animations?.map((a: any) => a?.name || a?.resource?.name || 'unnamed') || [],\n interactionConfig: meshConfig.interaction\n });\n\n // Setup animations if the model has them\n // APPROACH: Match HTML export - prioritize modelEntity.anim first, then fallback\n if (hasGlbAnimations || (meshConfig.interaction && meshConfig.interaction.playModelAnimation)) {\n const allAnimComponents: any[] = [];\n\n // PRIORITY 1: Check if PlayCanvas auto-created an anim component (like HTML export expects)\n const animComponent = (modelEntity as any).anim;\n if (animComponent) {\n console.log('[CustomMesh] Found modelEntity.anim component - using simple approach');\n allAnimComponents.push({\n type: 'pc-anim',\n component: animComponent,\n modelEntity: modelEntity\n });\n }\n\n // PRIORITY 2: Search hierarchy for anim/animation components\n if (allAnimComponents.length === 0) {\n function findAllAnimComponents(node: any, depth: number = 0): void {\n // Check for new Anim component\n if (node.anim && !allAnimComponents.find((a: any) => a.component === node.anim)) {\n allAnimComponents.push({ type: 'anim', component: node.anim, node: node });\n console.log('[CustomMesh] Found existing ANIM component on:', node.name);\n }\n\n // Check for legacy Animation component\n if (node.animation) {\n allAnimComponents.push({ type: 'animation', component: node.animation, node: node });\n console.log('[CustomMesh] Found ANIMATION (legacy) component on:', node.name);\n }\n\n if (node.children) {\n node.children.forEach((child: any) => findAllAnimComponents(child, depth + 1));\n }\n }\n findAllAnimComponents(modelEntity);\n }\n\n // PRIORITY 3: Fall back to manual GLB skeleton animation approach\n if (allAnimComponents.length === 0 && hasGlbAnimations) {\n const animations = asset.resource.animations;\n console.log('[CustomMesh] No anim component found - falling back to GLB skeleton approach');\n console.log('[CustomMesh] GLB has', animations.length, 'embedded animations');\n\n try {\n allAnimComponents.push({\n type: 'glb-skeleton',\n modelEntity: modelEntity,\n asset: asset,\n animations: animations,\n animationNames: animations.map((a: any) => a.resource?.name || a.name || 'Animation'),\n isPlaying: false,\n currentTime: 0\n });\n\n console.log('[CustomMesh] GLB skeleton animations stored:',\n animations.map((a: any) => a.resource?.name || a.name));\n\n } catch (err) {\n console.error('[CustomMesh] Error setting up GLB animations:', err);\n }\n }\n\n if (allAnimComponents.length > 0) {\n // Store all animation components (now with type info)\n (meshData as any).allAnimComponents = allAnimComponents;\n meshData.animComponent = allAnimComponents[0].component;\n\n console.log('[CustomMesh] Total animation components for', meshConfig.name, ':', allAnimComponents.length,\n '- Types:', allAnimComponents.map((a: any) => a.type).join(', '));\n\n // Auto-play if enabled OR if animationAutoPlay is set\n const shouldAutoPlay = meshConfig.interaction?.animationAutoPlay;\n if (shouldAutoPlay) {\n allAnimComponents.forEach((animInfo: any) => {\n playAnimComponentV2(animInfo);\n });\n meshData.isAnimPlaying = true;\n console.log('[CustomMesh] Auto-playing all animations for:', meshConfig.name);\n }\n } else {\n console.warn('[CustomMesh] No animation components could be set up for mesh:', meshConfig.name);\n }\n } else {\n console.log('[CustomMesh] No animations to setup for:', meshConfig.name, '(no GLB animations and playModelAnimation not enabled)');\n }\n\n // Setup spatial audio if configured\n if (meshConfig.interaction && meshConfig.interaction.playAudio && meshConfig.interaction.audioUrl) {\n setupMeshAudio(entity, meshConfig, meshData);\n }\n\n // Setup click interaction\n if (meshConfig.interaction) {\n setupMeshClickInteraction(entity, meshConfig, meshData);\n }\n\n console.log('[CustomMesh] Mesh fully initialized:', meshConfig.name);\n } catch (err) {\n console.error('[CustomMesh] Error processing loaded mesh:', meshConfig.name, err);\n }\n });\n\n modelAsset.on('error', (err: any) => {\n console.error('[CustomMesh] Failed to load mesh:', meshConfig.name, err);\n // Remove failed asset to prevent further issues\n app.assets.remove(modelAsset);\n });\n\n app.assets.load(modelAsset);\n\n // Visibility range setup\n if (meshConfig.visibilityRange) {\n (entity as any).visibilityRange = meshConfig.visibilityRange;\n entity.enabled = meshConfig.enabled !== false;\n } else {\n entity.enabled = meshConfig.enabled !== false;\n }\n\n // Opacity configuration\n (entity as any).opacityConfig = {\n mode: meshConfig.opacityMode || 'static',\n value: meshConfig.opacity !== undefined ? meshConfig.opacity : 1\n };\n\n app.root.addChild(entity);\n\n return entity;\n }\n\n /**\n * Setup spatial audio for mesh\n */\n function setupMeshAudio(entity: pc.Entity, meshConfig: any, meshData: CustomMeshData): void {\n const audioConfig = meshConfig.interaction;\n const meshId = meshConfig.id || meshConfig.name;\n const slotId = `mesh-audio-${meshId}`;\n\n // Add sound component to entity\n entity.addComponent('sound', {\n positional: audioConfig.audioSpatial || false,\n distanceModel: audioConfig.audioDistanceModel || 'exponential', // Match HTML export default\n refDistance: audioConfig.audioRefDistance || 1,\n maxDistance: audioConfig.audioMaxDistance || 100,\n rollOffFactor: audioConfig.audioRollOffFactor || 1,\n slots: {\n [slotId]: {\n name: slotId,\n loop: audioConfig.audioLoop || false,\n autoPlay: false,\n volume: audioConfig.audioVolume !== undefined ? audioConfig.audioVolume : 1,\n pitch: 1\n }\n }\n });\n\n meshData.audioSlotId = slotId;\n\n // Load audio asset\n const audioAsset = new pc.Asset(\n `mesh-audio-asset-${meshId}`,\n 'audio',\n { url: audioConfig.audioUrl }\n );\n\n app.assets.add(audioAsset);\n\n audioAsset.ready(() => {\n const slot = entity.sound?.slot(slotId);\n if (slot) {\n slot.asset = audioAsset.id;\n }\n console.log('[CustomMesh] Audio loaded for mesh:', meshConfig.name, 'Spatial:', audioConfig.audioSpatial);\n });\n\n audioAsset.on('error', (err: any) => {\n console.error('[CustomMesh] Failed to load mesh audio:', meshConfig.name, err);\n });\n\n app.assets.load(audioAsset);\n }\n\n /**\n * Check if a ray intersects any mesh render component in the entity hierarchy\n */\n function rayIntersectsMeshHierarchy(entity: pc.Entity, rayOrigin: pc.Vec3, rayDir: pc.Vec3): boolean {\n // Check this entity's render components\n const render = (entity as any).render;\n if (render && render.meshInstances) {\n for (const mi of render.meshInstances) {\n if (mi.aabb) {\n const intersection = rayIntersectsAABB(rayOrigin, rayDir, mi.aabb);\n if (intersection) return true;\n }\n }\n }\n\n // Check model component if present\n const model = (entity as any).model;\n if (model && model.meshInstances) {\n for (const mi of model.meshInstances) {\n if (mi.aabb) {\n const intersection = rayIntersectsAABB(rayOrigin, rayDir, mi.aabb);\n if (intersection) return true;\n }\n }\n }\n\n // Recursively check children\n const children = entity.children;\n if (children) {\n for (const child of children) {\n if (child instanceof pc.Entity && rayIntersectsMeshHierarchy(child, rayOrigin, rayDir)) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Ray-AABB intersection test\n */\n function rayIntersectsAABB(rayOrigin: pc.Vec3, rayDir: pc.Vec3, aabb: any): boolean {\n const min = aabb.getMin ? aabb.getMin() : aabb.min;\n const max = aabb.getMax ? aabb.getMax() : aabb.max;\n\n if (!min || !max) return false;\n\n let tmin = -Infinity;\n let tmax = Infinity;\n\n for (let i = 0; i < 3; i++) {\n const axis = ['x', 'y', 'z'][i];\n const origin = (rayOrigin as any)[axis];\n const dir = (rayDir as any)[axis];\n const minVal = (min as any)[axis] ?? min.data?.[i];\n const maxVal = (max as any)[axis] ?? max.data?.[i];\n\n if (Math.abs(dir) < 1e-8) {\n if (origin < minVal || origin > maxVal) return false;\n } else {\n let t1 = (minVal - origin) / dir;\n let t2 = (maxVal - origin) / dir;\n if (t1 > t2) [t1, t2] = [t2, t1];\n tmin = Math.max(tmin, t1);\n tmax = Math.min(tmax, t2);\n if (tmin > tmax) return false;\n }\n }\n\n return tmax >= 0;\n }\n\n /**\n * Check if a custom mesh has any active interactions\n * Matches HTML export's hasInteractions function\n */\n function hasInteractions(customMesh: any): boolean {\n const interaction = customMesh.interaction;\n if (!interaction) return false;\n return !!(\n interaction.triggerUIPopup ||\n interaction.playAudio ||\n interaction.playModelAnimation ||\n interaction.triggerDirectLink\n );\n }\n\n /**\n * Display mesh content using the hotspot popup system\n * Matches HTML export's displayMeshContent function\n */\n function displayMeshContent(customMesh: any): void {\n if (!customMesh.interaction) return;\n\n // Create a hotspot-like object to use with the hotspot popup system\n const hotspotData = {\n id: `mesh-content-${customMesh.id}`,\n title: customMesh.interaction.title || customMesh.name,\n information: customMesh.interaction.information,\n photoUrl: customMesh.interaction.photoUrl,\n iframeUrl: customMesh.interaction.iframeUrl,\n externalLinkUrl: customMesh.interaction.externalLinkUrl,\n externalLinkText: customMesh.interaction.externalLinkText,\n // Style options\n backgroundColor: customMesh.interaction.backgroundColor || '#000000',\n textColor: customMesh.interaction.textColor || '#ffffff',\n };\n\n // Use the viewer UI's hotspot popup if available, otherwise use our own popup\n if ((uiElements as any).showHotspotPopup) {\n (uiElements as any).showHotspotPopup(hotspotData);\n } else {\n showMeshPopup(customMesh);\n }\n }\n\n /**\n * Hide mesh content popup\n */\n function hideMeshContent(): void {\n // Remove hotspot popup if visible\n const hotspotPopup = document.querySelector('.storysplat-hotspot-popup');\n if (hotspotPopup) hotspotPopup.remove();\n\n // Remove mesh popup if visible\n const meshPopup = document.querySelector('.storysplat-mesh-popup');\n if (meshPopup) meshPopup.remove();\n }\n\n /**\n * Handle direct link trigger for a mesh\n * Matches HTML export's handleDirectLink function\n */\n function handleDirectLink(customMesh: any): void {\n if (!customMesh.interaction?.triggerDirectLink || !customMesh.interaction.directLinkUrl) return;\n window.open(customMesh.interaction.directLinkUrl, '_blank');\n }\n\n /**\n * Setup hover and click interactions for mesh\n * Enhanced to match HTML export's setupMeshInteractions with per-interaction trigger modes\n */\n function setupMeshClickInteraction(entity: pc.Entity, meshConfig: any, meshData: CustomMeshData): void {\n // Skip if no active interactions\n if (!hasInteractions(meshConfig)) {\n console.log('[CustomMesh] Skipping interaction setup - no active interactions for:', meshConfig.name);\n return;\n }\n\n // Get the global activation mode (fallback for per-interaction modes)\n const activationMode = meshConfig.interaction?.activationMode || 'click';\n\n // Click handler using canvas click + raycast\n const canvasForMesh = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n // Helper to perform raycast\n const performRaycast = (clientX: number, clientY: number): boolean => {\n const rect = canvasForMesh.getBoundingClientRect();\n const x = clientX - rect.left;\n const y = clientY - rect.top;\n\n // Raycast from camera through click point\n const from = camera.camera!.screenToWorld(x, y, camera.camera!.nearClip);\n const to = camera.camera!.screenToWorld(x, y, camera.camera!.farClip);\n const dir = new pc.Vec3().sub2(to, from).normalize();\n\n // Get the model entity (child of container entity)\n const modelEntity = (entity as any).modelEntity as pc.Entity | undefined;\n\n // Check intersection with model entity hierarchy (includes all children)\n if (modelEntity && rayIntersectsMeshHierarchy(modelEntity, from, dir)) {\n return true;\n }\n\n // Fallback: also check the container entity itself\n if (rayIntersectsMeshHierarchy(entity, from, dir)) {\n return true;\n }\n\n // Final fallback: distance-based check for small or simple meshes\n const meshPos = entity.getPosition();\n const toMesh = new pc.Vec3().sub2(meshPos, from);\n const t = toMesh.dot(dir);\n if (t > 0) {\n const closestPoint = new pc.Vec3().add2(from, dir.clone().mulScalar(t));\n const distance = closestPoint.distance(meshPos);\n const scale = entity.getLocalScale();\n const hitRadius = Math.max(scale.x, scale.y, scale.z) * 1.5; // Larger radius for fallback\n if (distance < hitRadius) {\n return true;\n }\n }\n\n return false;\n };\n\n // Get per-interaction trigger modes (matches HTML export pattern)\n const popupTriggerMode = meshConfig.interaction?.popupTriggerMode || activationMode;\n const audioTriggerMode = meshConfig.interaction?.audioTriggerMode || activationMode;\n const animationTriggerMode = meshConfig.interaction?.animationTriggerMode || activationMode;\n const directLinkTriggerMode = meshConfig.interaction?.directLinkTriggerMode || activationMode;\n\n // Track hover state for this mesh\n let isHovering = false;\n\n /**\n * Handle HOVER IN interactions\n * Matches HTML export's OnPointerOverTrigger behavior\n */\n const handleHoverIn = () => {\n // Change cursor to pointer\n canvasForMesh.style.cursor = 'pointer';\n\n // UI Popup - trigger on hover if popupTriggerMode is 'hover'\n if (popupTriggerMode === 'hover' && meshConfig.interaction?.triggerUIPopup) {\n displayMeshContent(meshConfig);\n }\n\n // Audio - trigger on hover if audioTriggerMode is 'hover'\n if (audioTriggerMode === 'hover' && meshConfig.interaction?.playAudio && meshData.audioSlotId) {\n const slot = entity.sound?.slot(meshData.audioSlotId);\n if (slot && !slot.isPlaying) {\n slot.play();\n meshData.audioPlaying = true;\n console.log('[CustomMesh] Playing audio on hover for:', meshConfig.name);\n }\n }\n\n // Animation - trigger on hover if animationTriggerMode is 'hover'\n if (animationTriggerMode === 'hover' && meshConfig.interaction?.playModelAnimation) {\n const allAnimComponents = (meshData as any).allAnimComponents as any[] | undefined;\n if (allAnimComponents && allAnimComponents.length > 0 && !meshData.isAnimPlaying) {\n allAnimComponents.forEach((animInfo: any) => playAnimComponentV2(animInfo));\n meshData.isAnimPlaying = true;\n console.log('[CustomMesh] Playing animation on hover for:', meshConfig.name);\n }\n }\n\n // Direct link - trigger on hover if directLinkTriggerMode is 'hover'\n if (directLinkTriggerMode === 'hover' && meshConfig.interaction?.triggerDirectLink) {\n handleDirectLink(meshConfig);\n }\n };\n\n /**\n * Handle HOVER OUT interactions\n * Matches HTML export's OnPointerOutTrigger behavior\n */\n const handleHoverOut = () => {\n // Reset cursor\n canvasForMesh.style.cursor = '';\n\n // UI Popup - hide on hover out if popupTriggerMode is 'hover'\n if (popupTriggerMode === 'hover' && meshConfig.interaction?.triggerUIPopup) {\n hideMeshContent();\n }\n\n // Audio - stop on hover out if audioTriggerMode is 'hover'\n if (audioTriggerMode === 'hover' && meshConfig.interaction?.playAudio && meshData.audioSlotId) {\n const slot = entity.sound?.slot(meshData.audioSlotId);\n if (slot && slot.isPlaying) {\n slot.stop();\n meshData.audioPlaying = false;\n console.log('[CustomMesh] Stopped audio on hover out for:', meshConfig.name);\n }\n }\n\n // Animation - pause on hover out if animationTriggerMode is 'hover'\n if (animationTriggerMode === 'hover' && meshConfig.interaction?.playModelAnimation) {\n const allAnimComponents = (meshData as any).allAnimComponents as any[] | undefined;\n if (allAnimComponents && allAnimComponents.length > 0 && meshData.isAnimPlaying) {\n allAnimComponents.forEach((animInfo: any) => pauseAnimComponentV2(animInfo));\n meshData.isAnimPlaying = false;\n console.log('[CustomMesh] Paused animation on hover out for:', meshConfig.name);\n }\n }\n };\n\n /**\n * Handle CLICK interactions\n * Matches HTML export's OnPickTrigger behavior with toggle support\n */\n const handleClick = () => {\n console.log('[CustomMesh] Clicked mesh:', meshConfig.name);\n\n // UI Popup - trigger on click if popupTriggerMode is 'click'\n if (popupTriggerMode === 'click' && meshConfig.interaction?.triggerUIPopup) {\n displayMeshContent(meshConfig);\n }\n\n // Animation - toggle on click if animationTriggerMode is 'click'\n if (animationTriggerMode === 'click' && meshConfig.interaction?.playModelAnimation) {\n const allAnimComponents = (meshData as any).allAnimComponents as any[] | undefined;\n if (allAnimComponents && allAnimComponents.length > 0) {\n if (meshData.isAnimPlaying) {\n allAnimComponents.forEach((animInfo: any) => pauseAnimComponentV2(animInfo));\n meshData.isAnimPlaying = false;\n console.log('[CustomMesh] Paused', allAnimComponents.length, 'animations on click for:', meshConfig.name);\n } else {\n allAnimComponents.forEach((animInfo: any) => playAnimComponentV2(animInfo));\n meshData.isAnimPlaying = true;\n console.log('[CustomMesh] Playing', allAnimComponents.length, 'animations on click for:', meshConfig.name);\n }\n }\n }\n\n // Audio - toggle on click if audioTriggerMode is 'click'\n if (audioTriggerMode === 'click' && meshConfig.interaction?.playAudio && meshData.audioSlotId) {\n const slot = entity.sound?.slot(meshData.audioSlotId);\n if (slot) {\n if (slot.isPlaying) {\n slot.pause();\n meshData.audioPlaying = false;\n console.log('[CustomMesh] Paused audio on click for:', meshConfig.name);\n } else {\n slot.play();\n meshData.audioPlaying = true;\n console.log('[CustomMesh] Playing audio on click for:', meshConfig.name);\n }\n }\n }\n\n // Direct link - trigger on click if directLinkTriggerMode is 'click'\n if (directLinkTriggerMode === 'click' && meshConfig.interaction?.triggerDirectLink) {\n handleDirectLink(meshConfig);\n }\n };\n\n // Click handler\n const meshClickHandler = (e: MouseEvent) => {\n if (performRaycast(e.clientX, e.clientY)) {\n handleClick();\n }\n };\n\n // Hover handler - handles both hover in and hover out\n const meshHoverHandler = (e: MouseEvent) => {\n const nowHovering = performRaycast(e.clientX, e.clientY);\n if (nowHovering && !isHovering) {\n isHovering = true;\n handleHoverIn();\n } else if (!nowHovering && isHovering) {\n isHovering = false;\n handleHoverOut();\n }\n };\n\n // Mouse leave handler to reset hover state\n const meshLeaveHandler = () => {\n if (isHovering) {\n isHovering = false;\n handleHoverOut();\n }\n };\n\n canvasForMesh.addEventListener('click', meshClickHandler);\n canvasForMesh.addEventListener('mousemove', meshHoverHandler);\n canvasForMesh.addEventListener('mouseleave', meshLeaveHandler);\n\n // Store handlers for cleanup\n (entity as any).meshClickHandler = meshClickHandler;\n (entity as any).meshHoverHandler = meshHoverHandler;\n (entity as any).meshLeaveHandler = meshLeaveHandler;\n }\n\n /**\n * Show mesh popup\n */\n function showMeshPopup(meshConfig: any): void {\n // Remove existing popup if any\n const existingPopup = document.querySelector('.storysplat-mesh-popup');\n if (existingPopup) existingPopup.remove();\n\n const popup = document.createElement('div');\n popup.className = 'storysplat-mesh-popup';\n popup.style.cssText = `\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background: rgba(0, 0, 0, 0.9);\n padding: 30px;\n border-radius: 12px;\n max-width: 600px;\n max-height: 80vh;\n overflow: auto;\n z-index: 100001;\n color: white;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `;\n\n const title = document.createElement('h2');\n title.textContent = meshConfig.name || 'Custom Mesh';\n title.style.cssText = 'margin-top: 0; margin-bottom: 16px;';\n popup.appendChild(title);\n\n if (meshConfig.interaction.popupContent) {\n const content = document.createElement('p');\n content.textContent = meshConfig.interaction.popupContent;\n content.style.cssText = 'margin: 0; line-height: 1.6;';\n popup.appendChild(content);\n }\n\n const closeBtn = document.createElement('button');\n closeBtn.textContent = '× Close';\n closeBtn.style.cssText = `\n position: absolute;\n top: 10px;\n right: 10px;\n background: rgba(255, 255, 255, 0.2);\n border: none;\n color: white;\n padding: 8px 16px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 16px;\n `;\n closeBtn.onclick = () => popup.remove();\n popup.appendChild(closeBtn);\n\n document.body.appendChild(popup);\n }\n\n /**\n * Update mesh visibility based on current waypoint/progress\n * Called when waypoint changes or progress updates\n */\n function updateCustomMeshVisibility(): void {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n\n customMeshEntities.forEach((meshData) => {\n const { entity, config: meshConfig } = meshData;\n const range = (entity as any).visibilityRange;\n\n if (range) {\n let visible = true;\n\n if (range.type === 'waypoint') {\n // Show only between specific waypoints\n visible = waypointIndex >= range.start && waypointIndex <= range.end;\n } else if (range.type === 'percentage') {\n // Show based on scroll percentage (0-100)\n visible = scrollPercent >= range.start && scrollPercent <= range.end;\n }\n\n entity.enabled = visible;\n }\n\n // Update opacity if animated\n if (meshConfig.opacityMode === 'animated' && meshConfig.opacityAnimation) {\n const anim = meshConfig.opacityAnimation;\n\n if (scrollPercent >= anim.startPercent && scrollPercent <= anim.endPercent) {\n const progress = (scrollPercent - anim.startPercent) / (anim.endPercent - anim.startPercent);\n const opacity = anim.startOpacity + (anim.endOpacity - anim.startOpacity) * progress;\n\n // Apply opacity to all mesh instances in the entity\n applyOpacityToCustomMesh(entity, opacity);\n }\n }\n\n // Update billboard state based on range (matches HTML export lines 664-674)\n if (meshConfig.billboard && meshConfig.billboardRange) {\n const bRange = meshConfig.billboardRange;\n let billboardActive = false;\n\n if (bRange.type === 'percentage') {\n billboardActive = scrollPercent >= bRange.start && scrollPercent <= bRange.end;\n } else if (bRange.type === 'waypoint') {\n billboardActive = waypointIndex >= bRange.start && waypointIndex <= bRange.end;\n }\n\n // Store billboard state for the update loop to use\n (entity as any)._billboardActive = billboardActive;\n } else if (meshConfig.billboard) {\n // Billboard always active if no range specified\n (entity as any)._billboardActive = true;\n }\n });\n }\n\n /**\n * Apply opacity to all mesh render components\n */\n function applyOpacityToCustomMesh(entity: pc.Entity, opacity: number): void {\n function applyToEntity(ent: pc.Entity): void {\n if ((ent as any).render && (ent as any).render.meshInstances) {\n (ent as any).render.meshInstances.forEach((meshInstance: any) => {\n if (meshInstance.material) {\n // Clone material if it's shared to avoid affecting other instances\n if (!meshInstance.material._isCloned) {\n meshInstance.material = meshInstance.material.clone();\n meshInstance.material._isCloned = true;\n }\n\n meshInstance.material.opacity = opacity;\n meshInstance.material.blendType = pc.BLEND_NORMAL;\n meshInstance.material.update();\n }\n });\n }\n\n // Apply to children\n ent.children.forEach((child: any) => applyToEntity(child));\n }\n\n applyToEntity(entity);\n }\n\n /**\n * Initialize all custom meshes from config\n */\n async function initCustomMeshes(): Promise<void> {\n if (!config.customMeshes || config.customMeshes.length === 0) {\n console.log('[StorySplat Viewer] No custom meshes to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.customMeshes.length} custom meshes...`);\n console.log('[StorySplat Viewer] All custom mesh configs:', config.customMeshes);\n\n // Defer mesh loading to ensure PlayCanvas is fully initialized\n // This helps avoid race conditions with the asset loader\n setTimeout(() => {\n let loadedCount = 0;\n let skippedCount = 0;\n\n config.customMeshes!.forEach((meshConfig: any, index: number) => {\n console.log(`[CustomMesh] Processing mesh ${index}:`, {\n name: meshConfig.name,\n enabled: meshConfig.enabled,\n hasModelUrl: !!meshConfig.modelUrl,\n modelUrl: meshConfig.modelUrl?.substring(0, 100) + '...'\n });\n\n if (meshConfig.enabled !== false) {\n try {\n const entity = loadCustomMesh(meshConfig, index);\n if (entity) {\n loadedCount++;\n } else {\n skippedCount++;\n console.warn(`[CustomMesh] Mesh ${meshConfig.name} returned null (likely missing modelUrl)`);\n }\n } catch (err) {\n console.error('[CustomMesh] Error loading mesh', meshConfig.name, ':', err);\n skippedCount++;\n }\n } else {\n console.log('[CustomMesh] Skipping disabled mesh:', meshConfig.name, '(enabled =', meshConfig.enabled, ')');\n skippedCount++;\n }\n });\n\n console.log(`[CustomMesh] Summary: ${loadedCount} loaded, ${skippedCount} skipped`);\n }, 100);\n\n console.log(`[StorySplat Viewer] ${config.customMeshes.length} custom meshes queued for loading`);\n }\n\n /**\n * Cleanup all custom meshes\n */\n function cleanupCustomMeshes(): void {\n const canvasForCleanup = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n customMeshEntities.forEach((meshData) => {\n const entity = meshData.entity;\n if ((entity as any).meshClickHandler) {\n canvasForCleanup.removeEventListener('click', (entity as any).meshClickHandler);\n }\n entity.destroy();\n });\n\n customMeshEntities.clear();\n }\n\n // Listen for progress updates to update mesh visibility\n events.on('progressUpdate', () => {\n updateCustomMeshVisibility();\n });\n\n // =====================================================\n // SKYBOX SYSTEM\n // =====================================================\n\n /**\n * Initialize skybox from config with IBL (Image-Based Lighting) support\n * Matches HTML export skyboxSystem.ts\n * Supports both nested (config.skybox.url) and flat (config.skyboxUrl) formats\n */\n function initSkybox(): void {\n // Support both config formats: nested object and flat properties\n const skyboxUrl = config.skybox?.url || config.skyboxUrl;\n if (!skyboxUrl) {\n console.log('[StorySplat Viewer] No skybox configured');\n return;\n }\n\n // Get rotation from either format (value is in radians)\n const skyboxRotation = config.skybox?.rotation ?? config.skyboxRotation ?? 0;\n const skyboxIntensity = config.skybox?.intensity ?? 1.0;\n const enableIBL = config.skybox?.enableIBL ?? true;\n\n console.log('[StorySplat Viewer] Creating skybox:', skyboxUrl, 'rotation:', skyboxRotation, 'rad =', skyboxRotation * (180 / Math.PI), 'deg', 'IBL:', enableIBL);\n\n // Check if URL is an HDR/EXR environment map or a regular image\n const isHDR = skyboxUrl.toLowerCase().includes('.hdr') || skyboxUrl.toLowerCase().includes('.exr');\n\n // Create a large sphere for the skybox (inverted normals) - visual display\n const skyboxEntity = new pc.Entity('skybox');\n\n // Create skybox material\n const skyboxMaterial = new pc.StandardMaterial();\n skyboxMaterial.useLighting = false;\n skyboxMaterial.cull = pc.CULLFACE_FRONT; // Render inside of sphere\n\n // Load skybox texture\n const textureAsset = new pc.Asset('skybox-texture', 'texture', { url: skyboxUrl });\n app.assets.add(textureAsset);\n\n textureAsset.ready((asset: any) => {\n const texture = asset.resource as pc.Texture;\n\n // Apply to skybox sphere material\n skyboxMaterial.emissiveMap = texture;\n skyboxMaterial.emissive = new pc.Color(skyboxIntensity, skyboxIntensity, skyboxIntensity);\n skyboxMaterial.update();\n console.log('[StorySplat Viewer] Skybox texture loaded');\n\n // Apply IBL if enabled - use the texture for environment lighting\n if (enableIBL) {\n try {\n // Set scene exposure for HDR content\n if (isHDR) {\n app.scene.exposure = skyboxIntensity;\n // Set tone mapping via rendering options (if available)\n (app.scene as any).toneMapping = pc.TONEMAP_ACES;\n console.log('[StorySplat Viewer] HDR tone mapping enabled');\n }\n\n // For IBL, we need to create environment lighting from the texture\n // PlayCanvas uses envAtlas for prefiltered environment maps\n // For a regular 2D texture, we apply ambient light color derived from it\n if (texture) {\n // Set ambient lighting based on skybox\n // Use a neutral ambient derived from skybox intensity\n app.scene.ambientLight = new pc.Color(\n 0.3 * skyboxIntensity,\n 0.3 * skyboxIntensity,\n 0.35 * skyboxIntensity\n );\n\n // If we have a cubemap (6-face or equirectangular HDR), set it as env atlas\n // For now, enhance ambient based on the skybox\n console.log('[StorySplat Viewer] IBL ambient lighting applied with intensity:', skyboxIntensity);\n }\n } catch (iblError) {\n console.warn('[StorySplat Viewer] Failed to apply IBL:', iblError);\n }\n }\n });\n\n textureAsset.on('error', (err: any) => {\n console.error('[StorySplat Viewer] Failed to load skybox texture:', err);\n });\n\n app.assets.load(textureAsset);\n\n // Add sphere component\n skyboxEntity.addComponent('render', {\n type: 'sphere',\n material: skyboxMaterial,\n castShadows: false,\n receiveShadows: false\n });\n\n // Scale to encompass scene (large sphere)\n skyboxEntity.setLocalScale(500, 500, 500);\n\n // Apply rotation\n if (skyboxRotation !== 0) {\n const rotationDegrees = skyboxRotation * (180 / Math.PI);\n skyboxEntity.setEulerAngles(0, rotationDegrees, 0);\n }\n\n // Make skybox follow camera position (so it always appears infinite)\n app.on('update', () => {\n const camPos = camera.getPosition();\n skyboxEntity.setPosition(camPos.x, camPos.y, camPos.z);\n });\n\n app.root.addChild(skyboxEntity);\n console.log('[StorySplat Viewer] Skybox created with rotation:', skyboxRotation, 'intensity:', skyboxIntensity);\n }\n\n // =====================================================\n // CUSTOM LIGHTING SYSTEM\n // =====================================================\n const lightEntities: pc.Entity[] = [];\n\n /**\n * Convert hex color to PlayCanvas Color\n */\n function hexToColorForLights(hex: string): pc.Color {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (result) {\n return new pc.Color(\n parseInt(result[1], 16) / 255,\n parseInt(result[2], 16) / 255,\n parseInt(result[3], 16) / 255\n );\n }\n return new pc.Color(1, 1, 1); // Default white\n }\n\n /**\n * Create point light\n */\n function createPointLight(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Point Light');\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_POINT,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n range: lightConfig.range || 10,\n castShadows: lightConfig.castShadows || false\n });\n\n // Position (negate Z for coordinate conversion)\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n entity.enabled = lightConfig.enabled !== false;\n return entity;\n }\n\n /**\n * Create directional light\n */\n function createDirectionalLightEntity(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Directional Light');\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_DIRECTIONAL,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n castShadows: lightConfig.castShadows || false\n });\n\n // Position (negate Z for coordinate conversion)\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n // Direction (rotation)\n const rot = lightConfig.rotation;\n if (rot) {\n const radToDeg = 180 / Math.PI;\n entity.setEulerAngles(\n (rot._x ?? rot.x ?? 0) * radToDeg,\n (rot._y ?? rot.y ?? 0) * radToDeg,\n (rot._z ?? rot.z ?? 0) * radToDeg\n );\n } else {\n entity.setEulerAngles(45, 0, 0);\n }\n\n entity.enabled = lightConfig.enabled !== false;\n return entity;\n }\n\n /**\n * Create hemispheric light (approximated)\n */\n function createHemisphericLight(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Hemispheric Light');\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_DIRECTIONAL,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n castShadows: false\n });\n\n // Position\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n // Point upwards for hemispheric effect\n entity.setEulerAngles(-90, 0, 0);\n\n entity.enabled = lightConfig.enabled !== false;\n\n // If ground color is specified, adjust scene ambient\n if (lightConfig.groundColor) {\n const groundColor = hexToColorForLights(lightConfig.groundColor);\n const skyColor = hexToColorForLights(lightConfig.color || '#ffffff');\n\n app.scene.ambientLight = new pc.Color(\n (skyColor.r + groundColor.r * 0.3) / 1.3,\n (skyColor.g + groundColor.g * 0.3) / 1.3,\n (skyColor.b + groundColor.b * 0.3) / 1.3\n );\n }\n\n return entity;\n }\n\n /**\n * Create ambient light\n */\n function createAmbientLight(lightConfig: any): null {\n const color = hexToColorForLights(lightConfig.color || '#404040');\n const intensity = lightConfig.intensity || 0.4;\n\n app.scene.ambientLight = new pc.Color(\n color.r * intensity,\n color.g * intensity,\n color.b * intensity\n );\n\n console.log('[StorySplat Viewer] Set ambient light:', lightConfig.name || 'Ambient Light');\n return null;\n }\n\n /**\n * Create spot light\n */\n function createSpotLight(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Spot Light');\n\n // Convert angles from radians to degrees if they come from the editor (stored in radians)\n // PlayCanvas expects degrees for cone angles\n const radToDeg = 180 / Math.PI;\n const angleRad = lightConfig.angle ?? (45 * Math.PI / 180); // Default to 45 degrees in radians\n const angleDeg = angleRad * radToDeg;\n\n // Inner cone angle is typically smaller than outer - use exponent to derive it\n const exponent = lightConfig.exponent ?? 2;\n const innerAngleDeg = lightConfig.innerConeAngle ?? lightConfig.innerAngle ?? (angleDeg * 0.8);\n const outerAngleDeg = lightConfig.outerConeAngle ?? lightConfig.outerAngle ?? angleDeg;\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_SPOT,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n range: lightConfig.range || 10,\n innerConeAngle: innerAngleDeg,\n outerConeAngle: outerAngleDeg,\n castShadows: lightConfig.castShadows || false,\n shadowBias: lightConfig.shadowBias ?? 0.05,\n normalOffsetBias: lightConfig.normalOffsetBias ?? 0.05\n });\n\n // Position (negate Z for coordinate conversion)\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n // Direction (rotation) - spot lights need to be pointed at their target\n const rot = lightConfig.rotation;\n if (rot) {\n const radToDeg = 180 / Math.PI;\n entity.setEulerAngles(\n (rot._x ?? rot.x ?? 0) * radToDeg,\n (rot._y ?? rot.y ?? 0) * radToDeg,\n (rot._z ?? rot.z ?? 0) * radToDeg\n );\n } else if (lightConfig.direction) {\n // Alternative: direction vector to rotation\n const dir = lightConfig.direction;\n const dirVec = new pc.Vec3(\n dir._x ?? dir.x ?? 0,\n dir._y ?? dir.y ?? -1,\n -(dir._z ?? dir.z ?? 0)\n ).normalize();\n // Calculate rotation from direction\n entity.lookAt(\n entity.getPosition().x + dirVec.x,\n entity.getPosition().y + dirVec.y,\n entity.getPosition().z + dirVec.z\n );\n } else {\n // Default: point downward\n entity.setEulerAngles(90, 0, 0);\n }\n\n entity.enabled = lightConfig.enabled !== false;\n return entity;\n }\n\n /**\n * Initialize all custom lights from config\n */\n function initCustomLights(): void {\n if (!config.lights || config.lights.length === 0) {\n console.log('[StorySplat Viewer] No custom lights to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.lights.length} custom lights...`);\n\n config.lights.forEach((lightConfig: any, index: number) => {\n let entity: pc.Entity | null = null;\n\n switch (lightConfig.type) {\n case 'point':\n entity = createPointLight(lightConfig);\n break;\n case 'directional':\n entity = createDirectionalLightEntity(lightConfig);\n break;\n case 'hemispheric':\n entity = createHemisphericLight(lightConfig);\n break;\n case 'ambient':\n createAmbientLight(lightConfig);\n break;\n case 'spot':\n entity = createSpotLight(lightConfig);\n break;\n default:\n console.warn('[StorySplat Viewer] Unknown light type:', lightConfig.type);\n return;\n }\n\n if (entity) {\n app.root.addChild(entity);\n lightEntities.push(entity);\n console.log(`[StorySplat Viewer] Created ${lightConfig.type} light:`, lightConfig.name || `Light ${index}`);\n }\n });\n\n console.log('[StorySplat Viewer] Lighting setup complete');\n }\n\n /**\n * Setup spatial audio for a hotspot using Web Audio API\n * Matches the HTML export's setupHotspotAudio function in hotspotSystem.ts\n */\n function setupHotspotAudio(entity: pc.Entity, hotspot: any): HotspotAudioElements | null {\n if (!hotspot.audioUrl) return null;\n\n const audio = document.createElement('audio');\n audio.src = hotspot.audioUrl;\n audio.loop = hotspot.audioLoop || false;\n audio.volume = hotspot.audioVolume !== undefined ? hotspot.audioVolume : 1;\n audio.crossOrigin = 'anonymous';\n\n allAudioElements.push(audio);\n\n if (hotspot.audioSpatial) {\n // Create AudioContext for spatial audio\n const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();\n allAudioContexts.push(audioCtx);\n\n const source = audioCtx.createMediaElementSource(audio);\n const panner = audioCtx.createPanner();\n\n // Apply spatial settings (matching HTML export)\n panner.panningModel = 'HRTF';\n panner.distanceModel = (hotspot.audioDistanceModel || 'linear') as DistanceModelType; // Match HTML export default\n panner.refDistance = hotspot.audioRefDistance !== undefined ? hotspot.audioRefDistance : 1;\n panner.maxDistance = hotspot.audioMaxDistance !== undefined ? hotspot.audioMaxDistance : 100;\n panner.rolloffFactor = hotspot.audioRolloffFactor !== undefined ? hotspot.audioRolloffFactor : 1;\n\n // Position audio at hotspot location\n const pos = entity.getPosition();\n panner.setPosition(pos.x, pos.y, pos.z);\n\n // Connect audio graph\n source.connect(panner);\n panner.connect(audioCtx.destination);\n\n // Update audio position each frame\n const updateAudioPosition = () => {\n if (!entity || !entity.getPosition) return;\n const hotspotPos = entity.getPosition();\n panner.setPosition(hotspotPos.x, hotspotPos.y, hotspotPos.z);\n\n // Update listener position to camera\n if (camera && camera.getPosition) {\n const camPos = camera.getPosition();\n const camForward = camera.forward;\n const camUp = camera.up;\n\n if (audioCtx.listener.positionX) {\n // Modern API\n audioCtx.listener.positionX.value = camPos.x;\n audioCtx.listener.positionY.value = camPos.y;\n audioCtx.listener.positionZ.value = camPos.z;\n audioCtx.listener.forwardX.value = camForward.x;\n audioCtx.listener.forwardY.value = camForward.y;\n audioCtx.listener.forwardZ.value = camForward.z;\n audioCtx.listener.upX.value = camUp.x;\n audioCtx.listener.upY.value = camUp.y;\n audioCtx.listener.upZ.value = camUp.z;\n } else {\n // Legacy API\n audioCtx.listener.setPosition(camPos.x, camPos.y, camPos.z);\n audioCtx.listener.setOrientation(\n camForward.x, camForward.y, camForward.z,\n camUp.x, camUp.y, camUp.z\n );\n }\n }\n };\n\n // Register update function\n app.on('update', updateAudioPosition);\n\n console.log(`[Audio] Spatial audio setup for hotspot: ${hotspot.title}, refDist=${panner.refDistance}, maxDist=${panner.maxDistance}`);\n\n return { audio, audioCtx, source, panner, updateAudioPosition };\n } else {\n // Non-spatial audio\n console.log(`[Audio] Non-spatial audio setup for hotspot: ${hotspot.title}`);\n return { audio };\n }\n }\n\n /**\n * Setup spatial audio for video hotspot using Web Audio API\n * Makes the video's audio 3D positional based on hotspot location\n */\n function setupVideoSpatialAudio(\n entity: pc.Entity,\n video: HTMLVideoElement,\n hotspot: any\n ): VideoSpatialAudio | null {\n // Check if video should have spatial audio\n if (!hotspot.videoSpatialAudio && hotspot.videoMuted !== false) {\n // No spatial audio requested and video is muted\n return null;\n }\n\n try {\n // Create AudioContext for spatial audio\n const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();\n allAudioContexts.push(audioCtx);\n\n // Create source from video element\n const source = audioCtx.createMediaElementSource(video);\n\n // Create panner for 3D positioning\n const panner = audioCtx.createPanner();\n panner.panningModel = 'HRTF';\n panner.distanceModel = (hotspot.videoDistanceModel || 'linear') as DistanceModelType; // Match HTML export default\n panner.refDistance = hotspot.videoRefDistance !== undefined ? hotspot.videoRefDistance : 1;\n panner.maxDistance = hotspot.videoMaxDistance !== undefined ? hotspot.videoMaxDistance : 100;\n panner.rolloffFactor = hotspot.videoRolloffFactor !== undefined ? hotspot.videoRolloffFactor : 1;\n\n // Position panner at hotspot location\n const pos = entity.getPosition();\n panner.setPosition(pos.x, pos.y, pos.z);\n\n // Connect: source -> panner -> destination\n source.connect(panner);\n panner.connect(audioCtx.destination);\n\n // Update panner and listener position each frame\n app.on('update', () => {\n if (!entity || !entity.getPosition) return;\n\n // Update panner position to hotspot location\n const hotspotPos = entity.getPosition();\n panner.setPosition(hotspotPos.x, hotspotPos.y, hotspotPos.z);\n\n // Update listener position to camera\n if (camera && camera.getPosition) {\n const camPos = camera.getPosition();\n const camForward = camera.forward;\n const camUp = camera.up;\n\n if (audioCtx.listener.positionX) {\n // Modern API\n audioCtx.listener.positionX.value = camPos.x;\n audioCtx.listener.positionY.value = camPos.y;\n audioCtx.listener.positionZ.value = camPos.z;\n audioCtx.listener.forwardX.value = camForward.x;\n audioCtx.listener.forwardY.value = camForward.y;\n audioCtx.listener.forwardZ.value = camForward.z;\n audioCtx.listener.upX.value = camUp.x;\n audioCtx.listener.upY.value = camUp.y;\n audioCtx.listener.upZ.value = camUp.z;\n } else {\n // Legacy API\n audioCtx.listener.setPosition(camPos.x, camPos.y, camPos.z);\n audioCtx.listener.setOrientation(\n camForward.x, camForward.y, camForward.z,\n camUp.x, camUp.y, camUp.z\n );\n }\n }\n });\n\n console.log(`[Audio] Video spatial audio setup for hotspot: ${hotspot.title}, refDist=${panner.refDistance}, maxDist=${panner.maxDistance}`);\n\n return { audioCtx, source, panner };\n } catch (err) {\n console.warn('[Audio] Failed to setup video spatial audio:', err);\n return null;\n }\n }\n\n /**\n * Setup waypoint audio for audio interactions\n * Matches the HTML export's audioSystem.ts\n */\n function setupWaypointAudio(): void {\n if (!config.waypoints || config.waypoints.length === 0) return;\n\n // Extract audio interactions from waypoints\n config.waypoints.forEach((waypoint: any, waypointIndex: number) => {\n if (waypoint.interactions && Array.isArray(waypoint.interactions)) {\n waypoint.interactions.forEach((interaction: any) => {\n if (interaction.type === 'audio' && interaction.data) {\n const data = interaction.data;\n const audioId = interaction.id;\n\n // Create audio entity\n const audioEntity = new pc.Entity(`waypoint-audio-${audioId}`);\n\n // Position at waypoint location (with Z negation for PlayCanvas)\n const wpPos = waypoint.position || { x: 0, y: 0, z: 0 };\n audioEntity.setPosition(\n wpPos._x ?? wpPos.x ?? waypoint.x ?? 0,\n wpPos._y ?? wpPos.y ?? waypoint.y ?? 1.6,\n -(wpPos._z ?? wpPos.z ?? waypoint.z ?? 0)\n );\n\n // Configure sound component\n const soundConfig: any = {\n slots: {\n [audioId]: {\n name: audioId,\n loop: data.loop || false,\n autoPlay: false, // We control playback via waypoint\n volume: data.volume !== undefined ? data.volume : 1,\n pitch: 1,\n positional: data.spatialSound || false,\n distanceModel: getPlayCanvasDistanceModel(data.distanceModel || 'exponential'),\n maxDistance: data.maxDistance || 10000,\n refDistance: data.refDistance || 1,\n rollOffFactor: data.rolloffFactor || 1\n }\n }\n };\n\n audioEntity.addComponent('sound', soundConfig);\n\n // Load audio asset\n const audioAsset = new pc.Asset(\n `waypoint-audio-asset-${audioId}`,\n 'audio',\n { url: data.url }\n );\n\n app.assets.add(audioAsset);\n\n audioAsset.ready(() => {\n const slot = audioEntity.sound?.slot(audioId);\n if (slot) {\n slot.asset = audioAsset.id;\n }\n // Mark as ready for proximity-based playback\n const audioDataRef = waypointAudioMap.get(audioId);\n if (audioDataRef) {\n audioDataRef.assetReady = true;\n }\n console.log(`[Audio] Waypoint audio loaded: ${audioId}, spatialSound=${data.spatialSound}, maxDistance=${data.maxDistance}`);\n });\n\n app.assets.load(audioAsset);\n\n // Add to scene\n app.root.addChild(audioEntity);\n\n // Store audio data\n waypointAudioMap.set(audioId, {\n entity: audioEntity,\n waypointIndex: waypointIndex,\n config: data,\n slotId: audioId,\n playing: false,\n autoplayTriggered: false,\n assetReady: false\n });\n\n console.log(`[Audio] Waypoint audio created: ${audioId} at waypoint ${waypointIndex}, spatial=${data.spatialSound}`);\n }\n });\n }\n });\n\n if (waypointAudioMap.size > 0) {\n console.log(`[StorySplat Viewer] Setup ${waypointAudioMap.size} waypoint audio sources`);\n }\n }\n\n // Map distance model string to PlayCanvas constant\n function getPlayCanvasDistanceModel(modelStr: string): string {\n switch (modelStr) {\n case 'linear':\n return 'linear';\n case 'inverse':\n return 'inverse';\n case 'exponential':\n default:\n return 'exponential';\n }\n }\n\n /**\n * Update waypoint audio based on current waypoint (for autoplay on waypoint entry)\n * Called when waypoint changes\n */\n function updateWaypointAudio(currentIndex: number, previousIndex: number): void {\n waypointAudioMap.forEach((audioData, audioId) => {\n const { entity, waypointIndex, config, slotId, autoplayTriggered } = audioData;\n const slot = entity.sound?.slot(slotId);\n if (!slot) return;\n\n // Check if we're entering this waypoint\n const isEntering = currentIndex === waypointIndex && previousIndex !== waypointIndex;\n\n // Check if we're leaving this waypoint\n const isLeaving = previousIndex === waypointIndex && currentIndex !== waypointIndex;\n\n // Handle autoplay on waypoint entry (only if autoplay is explicitly true)\n if (isEntering && config.autoplay && !autoplayTriggered) {\n console.log(`[Audio] Autoplay waypoint audio: ${audioId} at waypoint ${waypointIndex}`);\n if (!slot.isPlaying) {\n slot.play();\n audioData.playing = true;\n audioData.autoplayTriggered = true;\n }\n }\n\n // Handle stop on waypoint exit\n if (isLeaving && config.stopOnExit && audioData.playing) {\n console.log(`[Audio] Stopping waypoint audio on exit: ${audioId}`);\n if (slot.isPlaying) {\n slot.stop();\n audioData.playing = false;\n audioData.autoplayTriggered = false;\n }\n }\n });\n }\n\n /**\n * Update waypoint audio based on PROXIMITY to audio source\n * For spatial audio, plays when camera is within maxDistance of the audio source\n * Called every frame\n */\n function updateWaypointAudioProximity(): void {\n if (waypointAudioMap.size === 0) return;\n\n const cameraPos = camera.getPosition();\n\n waypointAudioMap.forEach((audioData, audioId) => {\n const { entity, config, slotId, assetReady, playing } = audioData;\n\n // Skip if asset not ready yet\n if (!assetReady) return;\n\n const slot = entity.sound?.slot(slotId);\n if (!slot) return;\n\n // For spatial audio, use proximity-based playback\n if (config.spatialSound) {\n const audioPos = entity.getPosition();\n const distance = cameraPos.distance(audioPos);\n const maxDistance = config.maxDistance || 20;\n\n // Play when entering proximity (within maxDistance)\n if (distance <= maxDistance && !playing) {\n console.log(`[Audio] Proximity play: ${audioId}, distance=${distance.toFixed(2)}, maxDistance=${maxDistance}`);\n slot.play();\n audioData.playing = true;\n }\n // Stop when leaving proximity (only if stopOnExit is true)\n else if (distance > maxDistance && playing && config.stopOnExit) {\n console.log(`[Audio] Proximity stop: ${audioId}, distance=${distance.toFixed(2)}`);\n slot.stop();\n audioData.playing = false;\n }\n }\n });\n }\n\n // Track which waypoints are \"active\" (camera within trigger distance)\n const activeWaypointTriggers = new Set<number>();\n\n /**\n * Check waypoint trigger distances and execute/reverse interactions\n * Called every frame in the update loop\n */\n function checkWaypointTriggerDistance(): void {\n if (!config.waypoints?.length) return;\n\n const cameraPos = camera.getPosition();\n\n config.waypoints.forEach((wp: any, index: number) => {\n const wpPos = new pc.Vec3(\n wp.position?.x ?? 0,\n wp.position?.y ?? 0,\n -(wp.position?.z ?? 0) // Z-axis negation for PlayCanvas\n );\n\n const distance = cameraPos.distance(wpPos);\n const triggerDist = wp.triggerDistance ?? 1.0;\n\n if (distance <= triggerDist) {\n // Entering trigger zone\n if (!activeWaypointTriggers.has(index)) {\n activeWaypointTriggers.add(index);\n console.log(`[StorySplat] Waypoint ${index} triggered (distance: ${distance.toFixed(2)}, threshold: ${triggerDist})`);\n executeWaypointInteractions(wp, index);\n }\n } else {\n // Exiting trigger zone\n if (activeWaypointTriggers.has(index)) {\n activeWaypointTriggers.delete(index);\n console.log(`[StorySplat] Waypoint ${index} exited`);\n reverseWaypointInteractions(wp, index);\n }\n }\n });\n }\n\n /**\n * Execute all interactions on a waypoint when entering trigger distance\n */\n function executeWaypointInteractions(waypoint: any, waypointIndex: number): void {\n if (!waypoint.interactions?.length) return;\n\n waypoint.interactions.forEach((interaction: any) => {\n if (interaction.type === 'audio') {\n // The audio ID matches interaction.id (set in setupWaypointAudio)\n const audioId = interaction.id;\n const audioData = waypointAudioMap.get(audioId);\n if (audioData && audioData.assetReady && !audioData.playing) {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n slot.play();\n audioData.playing = true;\n }\n }\n }\n // Info interactions would show popup (if implemented)\n });\n }\n\n /**\n * Reverse/stop interactions when exiting trigger distance\n */\n function reverseWaypointInteractions(waypoint: any, waypointIndex: number): void {\n if (!waypoint.interactions?.length) return;\n\n waypoint.interactions.forEach((interaction: any) => {\n if (interaction.type === 'audio') {\n const stopOnExit = interaction.data?.stopOnExit ?? false;\n if (stopOnExit) {\n // The audio ID matches interaction.id (set in setupWaypointAudio)\n const audioId = interaction.id;\n const audioData = waypointAudioMap.get(audioId);\n if (audioData && audioData.playing) {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n slot.stop();\n audioData.playing = false;\n }\n }\n }\n }\n // Info interactions would hide popup (if implemented)\n });\n }\n\n /**\n * Global mute/unmute functions\n */\n function muteAllAudio(): void {\n if (globalMuted) return;\n\n // Mute hotspot audio elements\n allAudioElements.forEach(audio => {\n storedVolumes.set(audio, audio.volume);\n audio.volume = 0;\n });\n\n // Mute waypoint audio\n waypointAudioMap.forEach((audioData) => {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n (slot as any)._storedVolume = slot.volume;\n slot.volume = 0;\n }\n });\n\n // Also mute any HTML audio/video elements\n document.querySelectorAll('audio, video').forEach((el) => {\n (el as HTMLMediaElement).muted = true;\n });\n\n globalMuted = true;\n console.log('[Audio] All audio muted');\n }\n\n function unmuteAllAudio(): void {\n if (!globalMuted) return;\n\n // Unmute hotspot audio elements\n allAudioElements.forEach(audio => {\n const storedVol = storedVolumes.get(audio);\n audio.volume = storedVol !== undefined ? storedVol : 1;\n });\n\n // Unmute waypoint audio\n waypointAudioMap.forEach((audioData) => {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n const storedVol = (slot as any)._storedVolume;\n slot.volume = storedVol !== undefined ? storedVol : 1;\n }\n });\n\n // Also unmute any HTML audio/video elements\n document.querySelectorAll('audio, video').forEach((el) => {\n (el as HTMLMediaElement).muted = false;\n });\n\n globalMuted = false;\n console.log('[Audio] All audio unmuted');\n }\n\n function toggleMuteAll(): boolean {\n if (globalMuted) {\n unmuteAllAudio();\n } else {\n muteAllAudio();\n }\n return globalMuted;\n }\n\n // Track animated GIF textures for cleanup\n const animatedGifs: AnimatedGifTexture[] = [];\n\n // Create hotspot material for images - entity is hidden until texture loads\n // Supports both static images and animated GIFs with transparency\n function createImageMaterial(\n imageUrl: string,\n useLighting: boolean = false,\n opacity: number = 1,\n onTextureReady?: () => void\n ): pc.StandardMaterial {\n const material = new pc.StandardMaterial();\n\n // Configure material for transparency with proper depth handling\n // Use premultiplied alpha blending with depth write for better GSplat compatibility\n material.blendType = pc.BLEND_PREMULTIPLIED;\n material.depthTest = true; // Enable depth testing so hotspots are occluded by splats\n material.depthWrite = true; // Enable depth write for proper GSplat sorting\n material.cull = pc.CULLFACE_NONE; // Double-sided rendering\n material.twoSidedLighting = true; // Proper lighting on both sides\n material.alphaTest = 0.01; // Discard nearly transparent pixels for better sorting\n\n // For unlit mode: disable all lighting calculations, use only emissive\n if (!useLighting) {\n // Disable diffuse lighting entirely\n material.diffuse = new pc.Color(0, 0, 0);\n // Full white emissive - texture will provide color\n material.emissive = new pc.Color(1, 1, 1);\n // Disable specular\n material.specular = new pc.Color(0, 0, 0);\n material.useLighting = false;\n } else {\n material.diffuse = new pc.Color(1, 1, 1);\n }\n\n material.opacity = opacity;\n material.update();\n\n // Check if this is a GIF - use AnimatedGifTexture for proper transparency and animation\n if (isGifUrl(imageUrl)) {\n console.log(`[Hotspot] Loading animated GIF: ${imageUrl}`);\n\n const gifTexture = new AnimatedGifTexture(app, imageUrl, {\n autoPlay: true,\n onReady: () => {\n if (isDestroyed) {\n console.log(`[Hotspot] Ignoring GIF load - viewer was destroyed: ${imageUrl}`);\n gifTexture.destroy();\n return;\n }\n\n if (gifTexture.texture) {\n // Assign texture to material based on lighting mode\n if (!useLighting) {\n material.emissiveMap = gifTexture.texture;\n material.opacityMap = gifTexture.texture;\n } else {\n material.diffuseMap = gifTexture.texture;\n material.opacityMap = gifTexture.texture;\n }\n material.update();\n\n console.log(`[Hotspot] GIF texture loaded: ${imageUrl}, useLighting=${useLighting}`);\n\n if (onTextureReady) {\n onTextureReady();\n }\n }\n },\n onError: (error) => {\n console.error(`[Hotspot] Failed to load GIF: ${imageUrl}`, error);\n // On error, make it slightly visible so user knows something is there\n material.opacity = 0.3;\n material.emissive = new pc.Color(1, 0, 0); // Red to indicate error\n material.update();\n if (onTextureReady) {\n onTextureReady(); // Still call callback so entity becomes visible\n }\n }\n });\n\n // Track for cleanup\n animatedGifs.push(gifTexture);\n } else {\n // Standard image loading\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n // Check if viewer was destroyed while image was loading (React StrictMode race condition)\n if (isDestroyed) {\n console.log(`[Hotspot] Ignoring texture load - viewer was destroyed: ${imageUrl}`);\n return;\n }\n\n // Create texture with the app's graphics device\n const texture = new pc.Texture(app.graphicsDevice, {\n width: img.width,\n height: img.height,\n format: pc.PIXELFORMAT_RGBA8,\n mipmaps: true,\n minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n\n // Upload the image data\n texture.setSource(img);\n\n // Assign texture to material based on lighting mode\n if (!useLighting) {\n // Unlit: only use emissive map (no diffuse lighting)\n material.emissiveMap = texture;\n material.opacityMap = texture;\n } else {\n // Lit: use diffuse map\n material.diffuseMap = texture;\n material.opacityMap = texture;\n }\n\n material.update();\n\n console.log(`[Hotspot] Texture loaded for: ${imageUrl}, useLighting=${useLighting}`);\n\n if (onTextureReady) {\n onTextureReady();\n }\n };\n img.onerror = (err) => {\n console.error(`[Hotspot] Failed to load texture: ${imageUrl}`, err);\n // On error, make it slightly visible so user knows something is there\n material.opacity = 0.3;\n material.emissive = new pc.Color(1, 0, 0); // Red to indicate error\n material.update();\n if (onTextureReady) {\n onTextureReady(); // Still call callback so entity becomes visible\n }\n };\n img.src = imageUrl;\n }\n\n return material;\n }\n\n // Create all hotspots\n function createHotspots(): void {\n if (!config.hotspots || config.hotspots.length === 0) {\n console.log('[StorySplat Viewer] No hotspots to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.hotspots.length} hotspots...`);\n\n config.hotspots.forEach((hotspot: any, index: number) => {\n const entity = new pc.Entity(`hotspot-${index}`);\n\n // Get position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n const pos = hotspot.position || { _x: 0, _y: 0, _z: 0 };\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n\n // Get scale\n const scale = hotspot.scale || { _x: 1, _y: 1, _z: 1 };\n const sx = Math.abs(scale._x ?? scale.x ?? 1);\n const sy = Math.abs(scale._y ?? scale.y ?? 1);\n const sz = scale._z ?? scale.z ?? 1;\n\n // Get rotation (convert from radians to degrees + 180° Y offset for BabylonJS -> PlayCanvas)\n const rot = hotspot.rotation || { _x: 0, _y: 0, _z: 0 };\n const radToDeg = 180 / Math.PI;\n const rotX = (rot._x ?? rot.x ?? 0) * radToDeg;\n const rotY = ((rot._y ?? rot.y ?? 0) * radToDeg) + 180;\n const rotZ = (rot._z ?? rot.z ?? 0) * radToDeg;\n entity.setEulerAngles(rotX, rotY, rotZ);\n\n // Create geometry based on type\n if (hotspot.type === 'sphere') {\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(hotspot.color || '#ffffff');\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // For spheres: use full opacity with depth write for proper GSplat occlusion\n // GSplat doesn't sort properly with transparent objects, so make spheres opaque-ish\n const sphereOpacity = hotspot.opacity ?? 0.8;\n if (sphereOpacity >= 0.95) {\n // Fully opaque - use standard opaque rendering\n material.opacity = 1;\n material.blendType = pc.BLEND_NONE;\n material.depthTest = true;\n material.depthWrite = true;\n } else {\n // Semi-transparent - use additive blending for better GSplat compatibility\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true; // Write depth to help with sorting\n }\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2); // Scale down spheres\n } else if (hotspot.type === 'image' && hotspot.imageUrl) {\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n // Get initial opacity (use startOpacity for animated, opacity for static)\n // Default to 1 (fully visible) - user must explicitly set startOpacity to 0 for fade-in\n let initialOpacity = 1;\n if (hotspot.opacityMode === 'animated' && hotspot.opacityAnimation) {\n // Use startOpacity if defined, otherwise default to 1 (visible)\n initialOpacity = hotspot.opacityAnimation.startOpacity !== undefined\n ? hotspot.opacityAnimation.startOpacity\n : 1;\n } else if (hotspot.opacity !== undefined) {\n initialOpacity = hotspot.opacity;\n }\n\n // Store target opacity for animation system\n (entity as any).targetOpacity = initialOpacity;\n (entity as any).textureLoaded = false;\n\n // HIDE entity until texture is loaded to prevent WebGL errors\n (entity as any).hiddenUntilTextureLoaded = true;\n entity.enabled = false;\n\n // Check useLighting setting from hotspot (default to false = unlit)\n const useLighting = hotspot.useLighting === true;\n\n const material = createImageMaterial(hotspot.imageUrl, useLighting, initialOpacity, () => {\n // Callback when texture is ready - show the entity\n (entity as any).textureLoaded = true;\n // Only enable if not hidden by visibility range\n if (!(entity as any).visibilityRange || (entity as any).shouldBeVisible) {\n entity.enabled = true;\n }\n (entity as any).hiddenUntilTextureLoaded = false;\n });\n entity.render!.material = material;\n entity.setLocalScale(sx, sy, sz);\n // Rotate plane to face forward - compensate for 180° Y offset in euler angles\n entity.rotateLocal(90, 180, 0);\n\n // Store material reference for opacity animation\n (entity as any).hotspotMaterial = material;\n\n console.log(`[Hotspot] Created image hotspot: ${hotspot.title}, opacity=${initialOpacity}, opacityMode=${hotspot.opacityMode}, useLighting=${useLighting}`);\n } else if (hotspot.type === 'video' && hotspot.videoUrl) {\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n // Detect iOS for alpha video method\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);\n\n // Determine which video URLs to use (iOS alpha support)\n const useAlphaMethod = (isIOS && hotspot.useIOSVideoAlphaMethod) || hotspot.forceIOSVideoAlphaMethodForAllDevices;\n const mainVideoUrl = useAlphaMethod && hotspot.iosMainVideoUrl ? hotspot.iosMainVideoUrl : hotspot.videoUrl;\n const alphaVideoUrl = useAlphaMethod ? (hotspot.alphaMaskVideoUrl || null) : null;\n\n // Detect if URL is a WebM file (which may contain alpha)\n const isWebMUrl = (url: string): boolean => {\n const lower = url.toLowerCase();\n return lower.endsWith('.webm') || lower.includes('format=webm') || lower.includes('video/webm');\n };\n\n // Check if this video is WebM (may have embedded alpha)\n const mainIsWebM = isWebMUrl(mainVideoUrl);\n const hasWebMAlpha = mainIsWebM && (hotspot.webmHasAlpha !== false); // Default true for WebM\n\n // Helper to create video element and texture\n const createVideoAndTexture = (url: string, isAlpha: boolean, useRGBA: boolean = false) => {\n const v = document.createElement('video');\n v.src = url;\n v.loop = hotspot.videoLoop !== false;\n v.crossOrigin = 'anonymous';\n v.playsInline = true;\n\n // Default muted state\n if (isAlpha) {\n v.muted = true; // Alpha video always muted\n } else {\n v.muted = hotspot.videoMuted !== false;\n }\n\n // Autoplay handling\n if (hotspot.mediaTriggerMode === 'autoplay') {\n v.autoplay = true;\n v.muted = true; // Autoplay requires muted (browser policy)\n }\n\n // Use RGBA format for WebM (alpha support) or standard RGB\n const t = new pc.Texture(app.graphicsDevice, {\n format: useRGBA ? pc.PIXELFORMAT_R8_G8_B8_A8 : pc.PIXELFORMAT_R8_G8_B8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n t.setSource(v);\n return { video: v, texture: t };\n };\n\n // Create main video and texture - use RGBA for WebM to support embedded alpha\n const main = createVideoAndTexture(mainVideoUrl, false, hasWebMAlpha);\n const video = main.video;\n const videoTexture = main.texture;\n\n // Create material - matching HTML export exactly\n const material = new pc.StandardMaterial();\n\n // Always use emissive for self-illumination (matches HTML export)\n material.diffuseMap = videoTexture;\n material.emissiveMap = videoTexture;\n material.emissive = new pc.Color(1, 1, 1);\n\n // Enable depth testing so hotspots are occluded by splats\n material.depthTest = true;\n material.depthWrite = false; // Disable depth write for transparent materials\n material.cull = pc.CULLFACE_NONE; // Double-sided rendering\n material.twoSidedLighting = true; // Proper lighting on both sides\n\n // WebM with embedded alpha - configure for transparency\n if (hasWebMAlpha) {\n material.opacityMap = videoTexture; // Alpha channel from same texture\n material.blendType = pc.BLEND_NORMAL;\n material.alphaTest = 0.01;\n console.log(`[Hotspot] WebM video with alpha enabled: ${hotspot.title}`);\n }\n\n // Alpha video support (iOS dual-video method) - takes precedence over WebM embedded alpha\n // iOS uses a separate grayscale video where luminance = alpha (chroma key technique)\n let alphaVideo: HTMLVideoElement | null = null;\n let alphaTexture: pc.Texture | null = null;\n\n if (alphaVideoUrl) {\n const alpha = createVideoAndTexture(alphaVideoUrl, true, false);\n alphaVideo = alpha.video;\n alphaTexture = alpha.texture;\n\n material.opacityMap = alphaTexture;\n // CRITICAL: Use 'r' channel for opacity since iOS alpha videos store\n // alpha as grayscale luminance in RGB, not in actual alpha channel\n // This matches HTML export's getAlphaFromRGB = true behavior\n material.opacityMapChannel = 'r';\n material.blendType = pc.BLEND_NORMAL;\n material.alphaTest = 0.01;\n\n // Synchronize alpha video with main video\n video.addEventListener('play', () => {\n if (alphaVideo && alphaVideo.paused) {\n alphaVideo.currentTime = video.currentTime;\n alphaVideo.play().catch(console.warn);\n }\n });\n video.addEventListener('pause', () => {\n if (alphaVideo && !alphaVideo.paused) {\n alphaVideo.pause();\n }\n });\n video.addEventListener('seeked', () => {\n if (alphaVideo) {\n alphaVideo.currentTime = video.currentTime;\n }\n });\n\n console.log(`[Hotspot] iOS alpha mask video enabled: ${hotspot.title}, alphaUrl=${alphaVideoUrl.substring(0, 50)}...`);\n }\n\n material.update();\n\n // Update textures each frame\n app.on('update', () => {\n if (video.readyState === video.HAVE_ENOUGH_DATA) {\n videoTexture.upload();\n }\n if (alphaVideo && alphaTexture && alphaVideo.readyState === alphaVideo.HAVE_ENOUGH_DATA) {\n alphaTexture.upload();\n }\n });\n\n // Aspect ratio scaling from video metadata\n const initialScale = { x: sx, y: sy, z: sz };\n video.addEventListener('loadedmetadata', () => {\n const width = video.videoWidth;\n const height = video.videoHeight;\n if (width > 0 && height > 0) {\n const ratio = width / height;\n // Only auto-adjust if scale is default (1,1)\n if (initialScale.x === 1 && initialScale.y === 1) {\n entity.setLocalScale(ratio * initialScale.y, initialScale.y, initialScale.z);\n }\n }\n });\n\n entity.render!.material = material;\n entity.setLocalScale(sx, sy, sz);\n // Rotate plane to face forward - compensate for 180° Y offset in euler angles\n entity.rotateLocal(90, 180, 0);\n\n // Store video references for playback control\n (entity as any).videoElement = video;\n (entity as any).alphaVideoElement = alphaVideo;\n (entity as any).hotspotMaterial = material;\n\n // Store media trigger mode and proximity settings\n (entity as any).mediaTriggerMode = hotspot.mediaTriggerMode || 'click';\n (entity as any).proximityDistance = hotspot.proximityDistance || 5;\n (entity as any).isVideoPlaying = false;\n\n // Setup spatial audio for video if not muted\n if (hotspot.videoMuted !== true) {\n const videoSpatialAudio = setupVideoSpatialAudio(entity, video, hotspot);\n if (videoSpatialAudio) {\n (entity as any).videoSpatialAudio = videoSpatialAudio;\n }\n }\n\n console.log(`[Hotspot] Created video hotspot: ${hotspot.title}, mode=${hotspot.mediaTriggerMode}, useAlpha=${!!alphaVideoUrl}, spatialAudio=${!!(entity as any).videoSpatialAudio}`);\n } else if (hotspot.type === 'gif' && hotspot.gifUrl) {\n // GIF hotspot implementation - animated texture using canvas\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n // Get initial opacity\n let initialOpacity = 1;\n if (hotspot.opacityMode === 'animated' && hotspot.opacityAnimation) {\n initialOpacity = hotspot.opacityAnimation.startOpacity !== undefined\n ? hotspot.opacityAnimation.startOpacity\n : 1;\n } else if (hotspot.opacity !== undefined) {\n initialOpacity = hotspot.opacity;\n }\n\n // Store state for GIF animation\n (entity as any).targetOpacity = initialOpacity;\n (entity as any).textureLoaded = false;\n (entity as any).hiddenUntilTextureLoaded = true;\n entity.enabled = false;\n\n // Check useLighting setting from hotspot (default to false = unlit)\n const useLighting = hotspot.useLighting === true;\n\n // Create material for GIF\n const material = new pc.StandardMaterial();\n material.blendType = pc.BLEND_NORMAL;\n material.opacity = initialOpacity;\n material.depthTest = true;\n material.depthWrite = false;\n material.cull = pc.CULLFACE_NONE;\n material.twoSidedLighting = true;\n\n if (!useLighting) {\n material.emissive = new pc.Color(1, 1, 1);\n material.diffuse = new pc.Color(0, 0, 0);\n }\n\n // Load GIF using SuperGif library pattern (parse frames)\n const loadAnimatedGif = async (gifUrl: string) => {\n try {\n // Create a canvas for GIF rendering\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d')!;\n\n // Load GIF as image first to get dimensions\n const img = new Image();\n img.crossOrigin = 'anonymous';\n\n img.onload = () => {\n canvas.width = img.width || 256;\n canvas.height = img.height || 256;\n\n // Draw static frame initially\n ctx.drawImage(img, 0, 0);\n\n // Create texture from canvas\n const texture = new pc.Texture(app.graphicsDevice, {\n format: pc.PIXELFORMAT_R8_G8_B8_A8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n texture.setSource(canvas);\n\n material.diffuseMap = texture;\n if (!useLighting) {\n material.emissiveMap = texture;\n }\n material.opacityMap = texture;\n material.alphaTest = 0.01;\n material.update();\n\n entity.render!.material = material;\n\n // Calculate aspect ratio\n const ratio = canvas.width / canvas.height;\n if (sx === 1 && sy === 1) {\n entity.setLocalScale(ratio, 1, sz);\n } else {\n entity.setLocalScale(sx, sy, sz);\n }\n\n // Rotate plane to face forward\n entity.rotateLocal(90, 180, 0);\n\n // Mark texture as loaded\n (entity as any).textureLoaded = true;\n (entity as any).gifCanvas = canvas;\n (entity as any).gifTexture = texture;\n (entity as any).hotspotMaterial = material;\n\n // Enable entity if not hidden by visibility range\n if (!(entity as any).visibilityRange || (entity as any).shouldBeVisible) {\n entity.enabled = true;\n }\n (entity as any).hiddenUntilTextureLoaded = false;\n\n // Try to load and animate the GIF frames using fetch + gif parsing\n fetch(gifUrl)\n .then(response => response.arrayBuffer())\n .then(buffer => {\n // Simple GIF frame extraction (will show animated if browser supports it via img tag)\n // For full animation support, we continuously redraw the img element\n let frameIndex = 0;\n const animateGif = () => {\n if (!entity.enabled || !(entity as any).gifTexture) return;\n\n // Redraw from the img which browsers animate automatically\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(img, 0, 0);\n (entity as any).gifTexture.upload();\n\n // Continue animation loop\n requestAnimationFrame(animateGif);\n };\n\n // Start animation loop\n (entity as any).gifAnimationFrame = requestAnimationFrame(animateGif);\n })\n .catch(err => {\n console.warn('[Hotspot] GIF animation load failed, using static frame:', err);\n });\n\n console.log(`[Hotspot] Created GIF hotspot: ${hotspot.title}, opacity=${initialOpacity}, useLighting=${useLighting}`);\n };\n\n img.onerror = (err) => {\n console.error('[Hotspot] Failed to load GIF:', hotspot.gifUrl, err);\n // Fallback to sphere\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n const fallbackMaterial = new pc.StandardMaterial();\n fallbackMaterial.diffuse = parseColor(hotspot.color || '#FF00FF');\n fallbackMaterial.opacity = 0.8;\n fallbackMaterial.blendType = pc.BLEND_NORMAL;\n fallbackMaterial.update();\n entity.render!.material = fallbackMaterial;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n entity.enabled = true;\n (entity as any).hiddenUntilTextureLoaded = false;\n };\n\n img.src = gifUrl;\n } catch (err) {\n console.error('[Hotspot] GIF hotspot creation failed:', err);\n }\n };\n\n loadAnimatedGif(hotspot.gifUrl);\n\n } else {\n // Default: sphere\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(hotspot.color || '#4CAF50');\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // Use additive blending for better GSplat compatibility\n const sphereOpacity = hotspot.opacity ?? 0.8;\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true;\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n }\n\n // Add collision for raycasting\n entity.addComponent('collision', {\n type: hotspot.type === 'sphere' ? 'sphere' : 'box',\n radius: 0.1,\n halfExtents: new pc.Vec3(0.5, 0.5, 0.05)\n });\n\n // Store hotspot data for click handling\n (entity as any).hotspotData = hotspot;\n\n // Setup hotspot audio (spatial or non-spatial)\n const audioElements = setupHotspotAudio(entity, hotspot);\n if (audioElements) {\n (entity as any).audioElements = audioElements;\n console.log(`[StorySplat Viewer] Audio setup for hotspot: ${hotspot.title || 'Untitled'}`);\n }\n\n // Billboard mode - make plane always face the camera (with optional range support)\n if (hotspot.billboard) {\n // Store original rotation for when billboard is disabled\n const originalRotation = entity.getEulerAngles().clone();\n\n // Check if billboard range is specified\n const hasBillboardRange = hotspot.billboardRangeStart !== undefined || hotspot.billboardRangeEnd !== undefined;\n\n // Initialize billboard active state\n (entity as any)._billboardActive = !hasBillboardRange; // Active by default if no range\n (entity as any)._billboardOriginalRotation = originalRotation;\n\n app.on('update', () => {\n // Only apply billboard if active (no range or within range)\n if ((entity as any)._billboardActive) {\n entity.lookAt(camera.getPosition());\n // Rotate plane to face camera: 90° X to orient plane face forward, 180° Y to flip towards camera\n entity.rotateLocal(90, 180, 0);\n }\n });\n }\n\n // Visibility range\n if (hotspot.visibilityRange) {\n (entity as any).visibilityRange = hotspot.visibilityRange;\n entity.enabled = false; // Start hidden\n }\n\n app.root.addChild(entity);\n hotspotEntities.push(entity);\n\n console.log(`[StorySplat Viewer] Created hotspot: ${hotspot.title || 'Untitled'}`);\n });\n }\n\n // Update hotspot visibility, opacity, and video playback based on progress\n function updateHotspotVisibility(): void {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n const cameraPos = camera.getPosition();\n\n hotspotEntities.forEach((entity: any) => {\n const hotspot = entity.hotspotData;\n if (!hotspot) return;\n\n // Determine if hotspot should be visible based on visibility range\n let shouldBeVisible = true;\n const range = entity.visibilityRange;\n if (range) {\n if (range.type === 'waypoint') {\n shouldBeVisible = waypointIndex >= range.start && waypointIndex <= range.end;\n } else if (range.type === 'percentage') {\n shouldBeVisible = scrollPercent >= range.start && scrollPercent <= range.end;\n }\n }\n\n // Store visibility state for texture load callback\n entity.shouldBeVisible = shouldBeVisible;\n\n // Update billboard state based on range (if billboard mode is enabled with range)\n if (hotspot.billboard && (hotspot.billboardRangeStart !== undefined || hotspot.billboardRangeEnd !== undefined)) {\n const rangeStart = hotspot.billboardRangeStart ?? 0;\n const rangeEnd = hotspot.billboardRangeEnd ?? 100;\n const billboardActive = scrollPercent >= rangeStart && scrollPercent <= rangeEnd;\n\n // Update billboard active state\n entity._billboardActive = billboardActive;\n\n // If billboard is deactivated and we have original rotation, restore it\n if (!billboardActive && entity._billboardOriginalRotation) {\n const origRot = entity._billboardOriginalRotation;\n entity.setEulerAngles(origRot.x, origRot.y, origRot.z);\n }\n }\n\n // Only enable if: should be visible AND (not an image OR texture is loaded)\n if (entity.hiddenUntilTextureLoaded) {\n // Image hotspot waiting for texture - keep hidden\n entity.enabled = false;\n } else {\n entity.enabled = shouldBeVisible;\n }\n\n // Handle video playback triggers\n if (entity.videoElement && hotspot.type === 'video') {\n const triggerMode = entity.mediaTriggerMode || 'click';\n\n // Proximity trigger: play when camera is within proximityDistance\n if (triggerMode === 'proximity') {\n const hotspotPos = entity.getPosition();\n const distance = cameraPos.distance(hotspotPos);\n const proximityDistance = entity.proximityDistance || 5;\n\n if (distance <= proximityDistance && !entity.isVideoPlaying) {\n // Enter proximity - play video\n playVideoHotspot(entity, hotspot);\n console.log(`[Hotspot] Proximity play: ${hotspot.title}, distance=${distance.toFixed(2)}`);\n } else if (distance > proximityDistance && entity.isVideoPlaying) {\n // Leave proximity - pause video\n pauseVideoHotspot(entity);\n console.log(`[Hotspot] Proximity pause: ${hotspot.title}, distance=${distance.toFixed(2)}`);\n }\n }\n\n // Scroll trigger: play when visible based on visibility range\n if (triggerMode === 'scroll') {\n if (shouldBeVisible && !entity.isVideoPlaying) {\n // Entered visibility range - play video\n playVideoHotspot(entity, hotspot);\n console.log(`[Hotspot] Scroll play: ${hotspot.title}, scroll=${scrollPercent.toFixed(1)}%`);\n } else if (!shouldBeVisible && entity.isVideoPlaying) {\n // Left visibility range - pause video\n pauseVideoHotspot(entity);\n console.log(`[Hotspot] Scroll pause: ${hotspot.title}, scroll=${scrollPercent.toFixed(1)}%`);\n }\n }\n }\n\n // Handle opacity animation (only if texture is loaded for image hotspots)\n if (hotspot.opacityMode === 'animated' && hotspot.opacityAnimation) {\n // Skip if this is an image hotspot and texture isn't loaded yet\n if (hotspot.type === 'image' && !entity.textureLoaded) {\n return;\n }\n\n const anim = hotspot.opacityAnimation;\n const startPercent = anim.startPercent ?? 0;\n const endPercent = anim.endPercent ?? 100;\n // Default to 1 (visible) if not explicitly set\n const startOpacity = anim.startOpacity !== undefined ? anim.startOpacity : 1;\n const endOpacity = anim.endOpacity !== undefined ? anim.endOpacity : 1;\n\n let opacity: number;\n if (scrollPercent <= startPercent) {\n opacity = startOpacity;\n } else if (scrollPercent >= endPercent) {\n opacity = endOpacity;\n } else {\n // Linear interpolation\n const animRange = endPercent - startPercent;\n const progress = (scrollPercent - startPercent) / animRange;\n opacity = startOpacity + (endOpacity - startOpacity) * progress;\n }\n\n opacity = Math.max(0, Math.min(1, opacity));\n\n // Update material opacity\n if (entity.hotspotMaterial) {\n entity.hotspotMaterial.opacity = opacity;\n entity.hotspotMaterial.update();\n } else if (entity.render && entity.render.material) {\n // Fallback to render material\n (entity.render.material as any).opacity = opacity;\n (entity.render.material as any).update();\n }\n }\n });\n }\n\n // Helper to play/pause video (defined here so updateHotspotVisibility can use them)\n function playVideoHotspot(entity: any, hotspot: any): void {\n const video = entity.videoElement as HTMLVideoElement;\n const alphaVideo = entity.alphaVideoElement as HTMLVideoElement | null;\n\n if (!video) return;\n\n // Set muted state based on hotspot settings (except for autoplay which is always muted initially)\n if (entity.mediaTriggerMode !== 'autoplay') {\n video.muted = hotspot.videoMuted !== false;\n }\n\n // Resume AudioContext for spatial audio (browser requires user interaction)\n if (entity.videoSpatialAudio && entity.videoSpatialAudio.audioCtx) {\n const audioCtx = entity.videoSpatialAudio.audioCtx as AudioContext;\n if (audioCtx.state === 'suspended') {\n audioCtx.resume().then(() => {\n console.log('[Audio] Video spatial audio context resumed');\n }).catch(err => console.warn('[Audio] Failed to resume video audio context:', err));\n }\n }\n\n video.play().catch(err => console.warn('Video play failed:', err));\n if (alphaVideo) {\n alphaVideo.play().catch(err => console.warn('Alpha video play failed:', err));\n }\n entity.isVideoPlaying = true;\n }\n\n function pauseVideoHotspot(entity: any): void {\n const video = entity.videoElement as HTMLVideoElement;\n const alphaVideo = entity.alphaVideoElement as HTMLVideoElement | null;\n\n if (!video) return;\n\n video.pause();\n if (alphaVideo) {\n alphaVideo.pause();\n }\n entity.isVideoPlaying = false;\n }\n\n // Listen for progress updates to update hotspot visibility\n events.on('progressUpdate', () => {\n updateHotspotVisibility();\n });\n\n // Also call initially after hotspots are created\n setTimeout(() => {\n updateHotspotVisibility();\n }, 100);\n\n // =====================================================\n // PORTAL CREATION AND RENDERING\n // =====================================================\n\n // Create all portals (similar to hotspots but for scene-to-scene navigation)\n function createPortals(): void {\n if (!config.portals || config.portals.length === 0) {\n console.log('[StorySplat Viewer] No portals to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.portals.length} portals...`);\n\n config.portals.forEach((portal: any, index: number) => {\n const entity = new pc.Entity(`portal-${index}`);\n\n // Get position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n const pos = portal.position || { _x: 0, _y: 0, _z: 0 };\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n\n // Get scale\n const scale = portal.scale || { _x: 1, _y: 1, _z: 1 };\n const sx = Math.abs(scale._x ?? scale.x ?? 1);\n const sy = Math.abs(scale._y ?? scale.y ?? 1);\n const sz = scale._z ?? scale.z ?? 1;\n\n // Get rotation (convert from radians to degrees + 180° Y offset for BabylonJS -> PlayCanvas)\n const rot = portal.rotation || { _x: 0, _y: 0, _z: 0 };\n const radToDeg = 180 / Math.PI;\n const rotX = (rot._x ?? rot.x ?? 0) * radToDeg;\n const rotY = ((rot._y ?? rot.y ?? 0) * radToDeg) + 180;\n const rotZ = (rot._z ?? rot.z ?? 0) * radToDeg;\n entity.setEulerAngles(rotX, rotY, rotZ);\n\n // Create geometry based on type (similar to hotspots)\n if (portal.type === 'sphere') {\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(portal.color || '#9C27B0'); // Purple default for portals\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // For spheres: use full opacity with depth write for proper GSplat occlusion\n const sphereOpacity = portal.opacity ?? 0.8;\n if (sphereOpacity >= 0.95) {\n material.opacity = 1;\n material.blendType = pc.BLEND_NONE;\n material.depthTest = true;\n material.depthWrite = true;\n } else {\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true;\n }\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n } else if (portal.type === 'image' && portal.imageUrl) {\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n const useLighting = portal.useLighting === true;\n const initialOpacity = portal.opacity ?? 1;\n\n (entity as any).textureLoaded = false;\n (entity as any).hiddenUntilTextureLoaded = true;\n entity.enabled = false;\n\n const material = createImageMaterial(portal.imageUrl, useLighting, initialOpacity, () => {\n (entity as any).textureLoaded = true;\n if (!(entity as any).visibilityRange || (entity as any).shouldBeVisible) {\n entity.enabled = true;\n }\n (entity as any).hiddenUntilTextureLoaded = false;\n });\n entity.render!.material = material;\n entity.setLocalScale(sx, sy, sz);\n entity.rotateLocal(90, 180, 0);\n\n (entity as any).portalMaterial = material;\n console.log(`[Portal] Created image portal: ${portal.title || portal.targetSceneName || 'Untitled'}`);\n } else {\n // Default: sphere\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(portal.color || '#9C27B0');\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // Use additive blending for better GSplat compatibility\n const sphereOpacity = portal.opacity ?? 0.8;\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true;\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n }\n\n // Add collision for raycasting\n entity.addComponent('collision', {\n type: portal.type === 'sphere' ? 'sphere' : 'box',\n radius: 0.1,\n halfExtents: new pc.Vec3(0.5, 0.5, 0.05)\n });\n\n // Store portal data for click handling\n (entity as any).portalData = portal;\n\n // Billboard mode\n if (portal.billboard) {\n app.on('update', () => {\n if (entity.enabled) {\n entity.lookAt(camera.getPosition());\n entity.rotateLocal(90, 180, 0);\n }\n });\n }\n\n // Visibility range\n if (portal.visibilityRange) {\n (entity as any).visibilityRange = portal.visibilityRange;\n entity.enabled = false;\n }\n\n app.root.addChild(entity);\n portalEntities.push(entity);\n\n console.log(`[StorySplat Viewer] Created portal: ${portal.title || portal.targetSceneName || 'Untitled'} -> ${portal.targetSceneId}`);\n });\n }\n\n // Update portal visibility based on progress\n function updatePortalVisibility(): void {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n const cameraPos = camera.getPosition();\n\n portalEntities.forEach((entity: any) => {\n const portal = entity.portalData;\n if (!portal) return;\n\n // Determine if portal should be visible based on visibility range\n let shouldBeVisible = true;\n const range = entity.visibilityRange;\n if (range) {\n if (range.type === 'waypoint') {\n shouldBeVisible = waypointIndex >= range.start && waypointIndex <= range.end;\n } else if (range.type === 'percentage') {\n shouldBeVisible = scrollPercent >= range.start && scrollPercent <= range.end;\n }\n }\n\n entity.shouldBeVisible = shouldBeVisible;\n\n // Enable/disable based on visibility and texture loaded state\n if (entity.hiddenUntilTextureLoaded) {\n entity.enabled = false;\n } else {\n entity.enabled = shouldBeVisible;\n }\n\n // Proximity activation check\n if (portal.activationMode === 'proximity' && shouldBeVisible) {\n const portalPos = entity.getPosition();\n const distance = cameraPos.distance(portalPos);\n const proximityDistance = portal.proximityDistance || 2;\n\n if (distance <= proximityDistance && !entity.proximityTriggered) {\n entity.proximityTriggered = true;\n console.log(`[Portal] Proximity triggered: ${portal.title || portal.targetSceneName}, navigating to scene ${portal.targetSceneId}`);\n handlePortalNavigation(portal);\n } else if (distance > proximityDistance) {\n entity.proximityTriggered = false;\n }\n }\n });\n }\n\n // Listen for progress updates to update portal visibility\n events.on('progressUpdate', () => {\n updatePortalVisibility();\n });\n\n // Also call initially after portals are created\n setTimeout(() => {\n updatePortalVisibility();\n }, 100);\n\n // Raycast helper for portal detection\n function raycastPortals(x: number, y: number): { entity: any; portal: any } | null {\n const from = camera.camera!.screenToWorld(x, y, camera.camera!.nearClip);\n const to = camera.camera!.screenToWorld(x, y, camera.camera!.farClip);\n\n let closestHit: { entity: any; distance: number } | null = null;\n\n portalEntities.forEach(entity => {\n if (!entity.enabled) return;\n\n const pos = entity.getPosition();\n const dir = new pc.Vec3().sub2(to, from).normalize();\n const toEntity = new pc.Vec3().sub2(pos, from);\n\n const t = toEntity.dot(dir);\n if (t < 0) return;\n\n const closestPoint = new pc.Vec3().add2(from, dir.clone().mulScalar(t));\n const distance = closestPoint.distance(pos);\n\n const entityScale = entity.getLocalScale();\n const hitRadius = Math.max(entityScale.x, entityScale.y, 0.3) * 0.6;\n\n if (distance < hitRadius) {\n if (!closestHit || t < closestHit.distance) {\n closestHit = { entity, distance: t };\n }\n }\n });\n\n if (closestHit !== null) {\n const entity = (closestHit as any).entity;\n return { entity, portal: entity.portalData };\n }\n return null;\n }\n\n // Handle portal navigation (scene-to-scene)\n async function handlePortalNavigation(portal: any): Promise<void> {\n if (!portal.targetSceneId) {\n console.warn('[Portal] No target scene ID specified');\n return;\n }\n\n console.log(`[StorySplat Viewer] Portal activated: navigating to scene ${portal.targetSceneId}`);\n\n // Emit event for external handlers\n events.emit('portalActivated', {\n portalId: portal.id,\n targetSceneId: portal.targetSceneId,\n targetSceneName: portal.targetSceneName\n });\n\n // Show loading UI\n showPortalLoadingUI(container, portal.targetSceneName || portal.targetSceneId);\n\n try {\n // Fetch the new scene data\n const sceneApiUrl = `https://discover.storysplat.com/api/scene/${portal.targetSceneId}`;\n console.log(`[Portal] Fetching scene from: ${sceneApiUrl}`);\n\n const response = await fetch(sceneApiUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch scene: ${response.status} ${response.statusText}`);\n }\n\n const result = await response.json();\n const newSceneData = result.data || result;\n\n // Store current instance reference before cleanup\n const currentInstance = instance;\n\n // Cleanup current scene (iOS memory management)\n cleanupForPortalNavigation();\n\n // Small delay for garbage collection (iOS)\n await new Promise(resolve => setTimeout(resolve, 100));\n\n // Create new viewer with the fetched scene data\n // Note: We're recreating in the same container, so we need to remove the current canvas first\n if (canvas && canvas.parentNode) {\n canvas.remove();\n }\n\n // Create new viewer (this will replace 'instance' reference for external code)\n const newViewer = await createViewer(container, newSceneData, {});\n\n // Hide loading UI\n hidePortalLoadingUI(container);\n\n console.log(`[Portal] Successfully navigated to scene: ${portal.targetSceneId}`);\n\n // Return the new viewer instance (for programmatic use)\n return newViewer as any;\n } catch (error) {\n console.error('[Portal] Navigation failed:', error);\n hidePortalLoadingUI(container);\n\n // Show error message\n const errorDiv = document.createElement('div');\n errorDiv.className = 'storysplat-portal-error';\n errorDiv.textContent = `Failed to load scene: ${(error as Error).message}`;\n Object.assign(errorDiv.style, {\n position: 'fixed',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n background: 'rgba(0,0,0,0.9)',\n color: '#ff6b6b',\n padding: '20px',\n borderRadius: '8px',\n zIndex: '10000',\n fontFamily: 'system-ui, sans-serif'\n });\n container.appendChild(errorDiv);\n setTimeout(() => errorDiv.remove(), 3000);\n }\n }\n\n // Cleanup for portal navigation (memory management)\n function cleanupForPortalNavigation(): void {\n console.log('[Portal] Cleaning up current scene for navigation...');\n\n // Stop playback\n pause();\n\n // Stop all hotspot videos\n hotspotEntities.forEach((entity: any) => {\n if (entity.videoElement) {\n entity.videoElement.pause();\n entity.videoElement.src = '';\n }\n if (entity.alphaVideoElement) {\n entity.alphaVideoElement.pause();\n entity.alphaVideoElement.src = '';\n }\n });\n\n // Cleanup animated GIFs\n animatedGifs.forEach(gif => gif.destroy());\n animatedGifs.length = 0;\n\n // Cleanup custom meshes\n cleanupCustomMeshes();\n\n // Destroy hotspot entities\n hotspotEntities.forEach(entity => {\n entity.destroy();\n });\n hotspotEntities.length = 0;\n\n // Destroy portal entities\n portalEntities.forEach(entity => {\n entity.destroy();\n });\n portalEntities.length = 0;\n\n // Destroy splat entity\n if (splatEntity) {\n splatEntity.destroy();\n splatEntity = null;\n }\n\n // Cleanup HTML Mesh Manager\n if ((app as any).__htmlMeshManager) {\n (app as any).__htmlMeshManager.destroy();\n }\n\n // Cleanup Custom Script System\n if ((app as any).__customScriptSystem) {\n (app as any).__customScriptSystem.dispose();\n }\n\n console.log('[Portal] Cleanup complete');\n }\n\n // Show portal loading UI\n function showPortalLoadingUI(container: HTMLElement, sceneName: string): void {\n // Remove any existing loading UI\n const existing = container.querySelector('.storysplat-portal-loading');\n if (existing) existing.remove();\n\n const loadingDiv = document.createElement('div');\n loadingDiv.className = 'storysplat-portal-loading';\n\n const spinner = document.createElement('div');\n spinner.className = 'storysplat-portal-spinner';\n\n const text = document.createElement('div');\n text.className = 'storysplat-portal-loading-text';\n text.textContent = `Loading ${sceneName}...`;\n\n loadingDiv.appendChild(spinner);\n loadingDiv.appendChild(text);\n\n // Styles\n Object.assign(loadingDiv.style, {\n position: 'fixed',\n top: '0',\n left: '0',\n width: '100%',\n height: '100%',\n background: 'rgba(0, 0, 0, 0.85)',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: '10000',\n fontFamily: 'system-ui, sans-serif'\n });\n\n Object.assign(spinner.style, {\n width: '50px',\n height: '50px',\n border: '4px solid rgba(255, 255, 255, 0.2)',\n borderTop: '4px solid #9C27B0',\n borderRadius: '50%',\n animation: 'storysplat-portal-spin 1s linear infinite',\n marginBottom: '20px'\n });\n\n Object.assign(text.style, {\n color: '#ffffff',\n fontSize: '18px'\n });\n\n // Add keyframes for spinner animation\n if (!document.getElementById('storysplat-portal-styles')) {\n const styleEl = document.createElement('style');\n styleEl.id = 'storysplat-portal-styles';\n styleEl.textContent = `\n @keyframes storysplat-portal-spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n }\n `;\n document.head.appendChild(styleEl);\n }\n\n container.appendChild(loadingDiv);\n }\n\n // Hide portal loading UI\n function hidePortalLoadingUI(container: HTMLElement): void {\n const loadingDiv = container.querySelector('.storysplat-portal-loading');\n if (loadingDiv) {\n loadingDiv.remove();\n }\n }\n\n // Raycast helper for hotspot detection\n const canvasEl = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n function raycastHotspots(x: number, y: number): { entity: any; hotspot: any } | null {\n const from = camera.camera!.screenToWorld(x, y, camera.camera!.nearClip);\n const to = camera.camera!.screenToWorld(x, y, camera.camera!.farClip);\n\n let closestHit: { entity: any; distance: number } | null = null;\n\n hotspotEntities.forEach(entity => {\n if (!entity.enabled) return;\n\n const pos = entity.getPosition();\n const dir = new pc.Vec3().sub2(to, from).normalize();\n const toEntity = new pc.Vec3().sub2(pos, from);\n\n const t = toEntity.dot(dir);\n if (t < 0) return;\n\n const closestPoint = new pc.Vec3().add2(from, dir.clone().mulScalar(t));\n const distance = closestPoint.distance(pos);\n\n // Use the entity's actual scale to determine hit radius\n // Take the maximum of X and Y scale for a reasonable hit area\n const entityScale = entity.getLocalScale();\n const hitRadius = Math.max(entityScale.x, entityScale.y, 0.3) * 0.6; // Min 0.18 units, scaled by 0.6\n\n if (distance < hitRadius) {\n if (!closestHit || t < closestHit.distance) {\n closestHit = { entity, distance: t };\n }\n }\n });\n\n if (closestHit !== null) {\n const entity = (closestHit as any).entity;\n return { entity, hotspot: entity.hotspotData };\n }\n return null;\n }\n\n // Hover detection for cursor change and hover popups\n let currentHoverHotspot: any = null;\n let isMouseOverPopup = false;\n\n // Track when mouse is over the popup\n const popup = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n const overlay = container.querySelector('.storysplat-hotspot-overlay') as HTMLElement;\n\n if (popup) {\n popup.addEventListener('mouseenter', () => {\n isMouseOverPopup = true;\n });\n popup.addEventListener('mouseleave', () => {\n isMouseOverPopup = false;\n // Close popup when mouse leaves the popup (for hover-activated hotspots)\n if (currentHoverHotspot && currentHoverHotspot.activationMode === 'hover') {\n popup.classList.remove('visible');\n if (overlay) overlay.classList.remove('visible');\n currentHoverHotspot = null;\n }\n });\n }\n\n canvasEl.addEventListener('mousemove', (e: MouseEvent) => {\n const rect = canvasEl.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Check portals first\n const portalHit = raycastPortals(x, y);\n if (portalHit && portalHit.portal) {\n const portal = portalHit.portal;\n // Show pointer cursor for click-activated portals\n const effectiveActivationMode = portal.activationMode || 'click';\n if (effectiveActivationMode === 'click') {\n canvasEl.style.cursor = 'pointer';\n } else {\n canvasEl.style.cursor = 'default';\n }\n return; // Portals take precedence over hotspots\n }\n\n const hit = raycastHotspots(x, y);\n if (hit && hit.hotspot) {\n const hotspot = hit.hotspot;\n\n // Show pointer cursor for interactive hotspots\n if (hotspot.activationMode === 'click' || hotspot.activationMode === 'hover' || hotspot.type === 'video') {\n canvasEl.style.cursor = 'pointer';\n } else {\n canvasEl.style.cursor = 'default';\n }\n\n // Show popup on hover for hover-activated hotspots\n if (hotspot.activationMode === 'hover' && currentHoverHotspot !== hotspot) {\n currentHoverHotspot = hotspot;\n if (hotspot.information || hotspot.photoUrl || hotspot.iframeUrl || hotspot.externalLinkUrl) {\n showHotspotPopup(container, hotspot);\n }\n }\n } else {\n canvasEl.style.cursor = 'default';\n\n // Only hide popup if mouse is NOT over the popup element\n if (currentHoverHotspot && currentHoverHotspot.activationMode === 'hover' && !isMouseOverPopup) {\n const popupEl = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n const overlayEl = container.querySelector('.storysplat-hotspot-overlay') as HTMLElement;\n if (popupEl) popupEl.classList.remove('visible');\n if (overlayEl) overlayEl.classList.remove('visible');\n currentHoverHotspot = null;\n }\n }\n });\n\n // Click handling for hotspots and portals\n canvasEl.addEventListener('click', (e: MouseEvent) => {\n const rect = canvasEl.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Check for portal clicks first (portals take precedence)\n const portalHit = raycastPortals(x, y);\n if (portalHit !== null && portalHit.portal) {\n const portal = portalHit.portal;\n console.log('[StorySplat Viewer] Portal clicked:', portal.title || portal.targetSceneName);\n\n // Handle click-activated portals\n const effectiveActivationMode = portal.activationMode || 'click';\n if (effectiveActivationMode === 'click') {\n handlePortalNavigation(portal);\n }\n return; // Don't process hotspot clicks when portal is clicked\n }\n\n const hitResult = raycastHotspots(x, y);\n if (hitResult !== null) {\n const entity = hitResult.entity;\n const hotspot = hitResult.hotspot;\n\n console.log('[StorySplat Viewer] Hotspot clicked:', hotspot.title);\n\n // Handle hotspot audio play/pause (spatial or non-spatial)\n if (entity.audioElements && entity.audioElements.audio) {\n const audioElements = entity.audioElements as HotspotAudioElements;\n const audio = audioElements.audio;\n\n if (audio.paused) {\n // Resume AudioContext on user interaction (browser requirement)\n if (audioElements.audioCtx && audioElements.audioCtx.state === 'suspended') {\n audioElements.audioCtx.resume();\n }\n audio.play().catch(err => console.error('[Audio] Hotspot audio play failed:', err));\n console.log('[Audio] Hotspot audio started:', hotspot.title);\n } else {\n audio.pause();\n console.log('[Audio] Hotspot audio paused:', hotspot.title);\n }\n }\n\n // Handle video toggle (for click or autoplay mode - autoplay needs click to unmute)\n if (entity.videoElement && (entity.mediaTriggerMode === 'click' || entity.mediaTriggerMode === 'autoplay')) {\n const video = entity.videoElement as HTMLVideoElement;\n const alphaVideo = entity.alphaVideoElement as HTMLVideoElement | null;\n\n if (entity.mediaTriggerMode === 'autoplay' && !video.paused && video.muted) {\n // Autoplay mode: click unmutes if already playing\n video.muted = hotspot.videoMuted === false ? false : false; // Unmute on click\n console.log('[Hotspot] Video unmuted by click');\n } else if (video.paused) {\n // Play video\n playVideoHotspot(entity, hotspot);\n console.log('[Hotspot] Video started');\n } else {\n // Pause video\n pauseVideoHotspot(entity);\n console.log('[Hotspot] Video paused');\n }\n }\n\n // Show popup for click-activated hotspots with content\n // Default to 'click' if activationMode is not set\n const effectiveActivationMode = hotspot.activationMode || 'click';\n // Show popup if there's any displayable content (title, information, photo, iframe, or link)\n const hasPopupContent = hotspot.title || hotspot.information || hotspot.photoUrl || hotspot.iframeUrl || hotspot.externalLinkUrl;\n if (effectiveActivationMode === 'click' && hasPopupContent) {\n showHotspotPopup(container, hotspot);\n }\n\n // Handle hotspot teleportation (teleport to waypoint or percentage)\n const teleportWaypoint = hotspot.teleportWaypoint ?? (hotspot as any).teleportToWaypoint;\n const teleportPercent = hotspot.teleportPercent ?? (hotspot as any).teleportToPercent;\n const teleportMode = hotspot.teleportMode || 'animate'; // Default to animate for smooth experience\n\n let calculatedTargetProgress: number | null = null;\n\n if (teleportWaypoint !== undefined && teleportWaypoint !== -1) {\n // Teleport to specific waypoint\n const numWaypoints = config.waypoints?.length || 1;\n const targetIndex = Math.max(0, Math.min(teleportWaypoint, numWaypoints - 1));\n calculatedTargetProgress = numWaypoints > 1 ? targetIndex / (numWaypoints - 1) : 0;\n console.log('[Hotspot] Teleporting to waypoint:', targetIndex, '(progress:', calculatedTargetProgress, ', mode:', teleportMode, ')');\n } else if (teleportPercent !== undefined && teleportPercent !== -1) {\n // Teleport to specific percentage (0-100)\n calculatedTargetProgress = Math.max(0, Math.min(teleportPercent / 100, 1));\n console.log('[Hotspot] Teleporting to percent:', teleportPercent, '(progress:', calculatedTargetProgress, ', mode:', teleportMode, ')');\n }\n\n if (calculatedTargetProgress !== null) {\n if (teleportMode === 'instant') {\n // Instant jump - directly set position\n currentProgress = calculatedTargetProgress;\n updateCameraFromProgress(currentProgress);\n } else {\n // Animate (default) - smooth transition\n animateToProgress(calculatedTargetProgress, 800); // 800ms for smooth transition\n }\n }\n }\n });\n\n // Double-click/double-tap to focus camera on clicked point (explore mode only)\n async function handleDoubleClickFocus(screenX: number, screenY: number): Promise<void> {\n if (currentCameraMode !== 'explore') return;\n\n console.log('[StorySplat Viewer] Double-click focus at:', screenX, screenY);\n\n // First check if we hit a hotspot\n const hotspotHit = raycastHotspots(screenX, screenY);\n if (hotspotHit) {\n const pos = hotspotHit.entity.getPosition();\n console.log('[StorySplat Viewer] Focusing on hotspot at:', pos.x, pos.y, pos.z);\n cameraControls.focus(pos, false);\n return;\n }\n\n // Try to pick the GSplat using pc.Picker with depth buffer (PlayCanvas 2.14+)\n try {\n // Use 25% scale for performance (matching PlayCanvas official picking example)\n const pickerScale = 0.25;\n picker.resize(\n Math.floor(canvasEl.clientWidth * pickerScale),\n Math.floor(canvasEl.clientHeight * pickerScale)\n );\n\n const worldLayer = app.scene.layers.getLayerByName('World');\n if (!worldLayer) {\n console.warn('[StorySplat Viewer] World layer not found');\n return;\n }\n\n picker.prepare(camera.camera!, app.scene, [worldLayer]);\n\n // Calculate scaled pick coordinates\n const scaledX = Math.floor(screenX * pickerScale);\n const scaledY = Math.floor(screenY * pickerScale);\n\n // Use getWorldPointAsync to get the actual 3D intersection point directly\n // This is the proper way to pick splats with depth-enabled picker\n const worldPoint = await picker.getWorldPointAsync(scaledX, scaledY);\n\n if (worldPoint) {\n // Validate the point is reasonable (not at infinity or behind camera)\n const cameraPos = camera.getPosition();\n const distance = cameraPos.distance(worldPoint);\n\n if (distance > 0.1 && distance < 1000) {\n console.log('[StorySplat Viewer] Focusing on GSplat at:',\n worldPoint.x.toFixed(2), worldPoint.y.toFixed(2), worldPoint.z.toFixed(2),\n 'distance:', distance.toFixed(2));\n cameraControls.focus(worldPoint, false);\n return;\n }\n }\n\n // Fallback: try mesh instance selection if world point failed\n const meshInstances = await picker.getSelectionAsync(scaledX, scaledY, 1, 1);\n\n if (meshInstances.length > 0) {\n const mi = meshInstances[0];\n // Use AABB center as a rough focus point\n const aabbCenter = mi.aabb.center.clone();\n console.log('[StorySplat Viewer] Focusing on mesh AABB center:',\n aabbCenter.x.toFixed(2), aabbCenter.y.toFixed(2), aabbCenter.z.toFixed(2));\n cameraControls.focus(aabbCenter, false);\n } else {\n console.log('[StorySplat Viewer] No pick result at click point');\n }\n } catch (err) {\n console.warn('[StorySplat Viewer] Picking failed:', err);\n }\n }\n\n // Desktop double-click handler\n canvasEl.addEventListener('dblclick', (e: MouseEvent) => {\n const rect = canvasEl.getBoundingClientRect();\n handleDoubleClickFocus(e.clientX - rect.left, e.clientY - rect.top);\n });\n\n // Mobile double-tap detection\n let lastTapTime = 0;\n const DOUBLE_TAP_THRESHOLD = 300;\n\n canvasEl.addEventListener('touchend', (e: TouchEvent) => {\n if (e.changedTouches.length !== 1) return;\n const now = Date.now();\n if (now - lastTapTime < DOUBLE_TAP_THRESHOLD) {\n const touch = e.changedTouches[0];\n const rect = canvasEl.getBoundingClientRect();\n handleDoubleClickFocus(touch.clientX - rect.left, touch.clientY - rect.top);\n lastTapTime = 0;\n } else {\n lastTapTime = now;\n }\n });\n\n // Load the splat after app starts\n updateProgress(0.2, 'Initializing...');\n\n loadSplat().then(() => {\n updateProgress(1.0, 'Ready!');\n\n // Create hotspots after splat is loaded\n createHotspots();\n\n // Create portals for scene-to-scene navigation\n createPortals();\n\n // Setup waypoint audio (audio interactions on waypoints)\n setupWaypointAudio();\n\n // Initialize particle systems\n console.log('[StorySplat Viewer] 🎆 Calling initParticleSystems...');\n initParticleSystems();\n\n // Initialize custom meshes (3D models with animations, audio)\n initCustomMeshes();\n\n // Initialize skybox\n initSkybox();\n\n // Initialize custom lights\n initCustomLights();\n\n // Start reveal effect IMMEDIATELY (before preloader hides)\n // The preloader is semi-transparent (0.85 opacity) so users can see the reveal through it\n if (revealScript) {\n revealScript.enabled = true;\n console.log('[StorySplat Viewer] Reveal effect started immediately');\n }\n\n // Hide preloader AFTER a short delay to let reveal effect start\n // This ensures users see the beginning of the reveal animation through the semi-transparent preloader\n setTimeout(() => {\n if (uiElements.preloader) {\n hidePreloader(uiElements.preloader);\n }\n }, 200);\n\n // Setup XR (VR/AR) if enabled\n setupXR();\n\n // Setup HTML Meshes if configured\n if (config.htmlMeshes && config.htmlMeshes.length > 0) {\n console.log('[StorySplat Viewer] Setting up HTML meshes:', config.htmlMeshes.length);\n const htmlMeshManager = setupHtmlMeshes(app, config.htmlMeshes);\n // Store manager reference for cleanup\n (app as any).__htmlMeshManager = htmlMeshManager;\n\n // Listen for progress updates to update HTML mesh visibility\n events.on('progressUpdate', () => {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n htmlMeshManager.updateVisibility(scrollPercent, waypointIndex);\n });\n }\n\n // Setup Custom Script System\n if (config.customScript && config.customScript.trim() !== '') {\n console.log('[StorySplat Viewer] Initializing custom script system...');\n const customScriptSystem = setupCustomScript(\n app,\n camera,\n canvas,\n config.customScript,\n () => currentProgress,\n () => currentWaypointIndex,\n () => hotspotEntities,\n () => splatEntity ? [splatEntity] : [],\n () => config.htmlMeshes || []\n );\n if (customScriptSystem) {\n // Store for cleanup\n (app as any).__customScriptSystem = customScriptSystem;\n console.log('[StorySplat Viewer] Custom script system initialized');\n }\n }\n\n // Check for URL parameters to support deep-linking\n // ?waypoint=2 will navigate to waypoint index 2\n // ?autoplay=true will start autoplay\n try {\n const urlParams = new URLSearchParams(window.location.search);\n const waypointParam = urlParams.get('waypoint');\n const autoplayParam = urlParams.get('autoplay');\n\n if (waypointParam !== null && config.waypoints && config.waypoints.length > 0) {\n const targetIndex = parseInt(waypointParam, 10);\n if (!isNaN(targetIndex) && targetIndex >= 0 && targetIndex < config.waypoints.length) {\n console.log('[StorySplat Viewer] URL param: navigating to waypoint', targetIndex);\n goToWaypoint(targetIndex);\n }\n }\n\n if (autoplayParam === 'true' && !options.autoPlay && !config.autoPlay) {\n console.log('[StorySplat Viewer] URL param: starting autoplay');\n play();\n }\n } catch (e) {\n // URLSearchParams may not be available in some environments\n console.warn('[StorySplat Viewer] Could not parse URL parameters:', e);\n }\n\n events.emit('ready');\n console.log('[StorySplat Viewer] Ready');\n\n // Connect UI to viewer controls\n if (showUI) {\n connectUIToViewer(uiElements, {\n nextWaypoint,\n prevWaypoint,\n play,\n pause,\n isPlaying: () => isPlaying,\n getCurrentWaypointIndex: () => currentWaypointIndex,\n getWaypointCount: () => config.waypoints?.length || 0,\n getWaypoints: () => config.waypoints || [],\n setCameraMode,\n on: (event, callback) => events.on(event, callback)\n }, defaultMode, uiOpts.buttonLabels);\n\n // Setup return to waypoint button click handler\n if (uiElements.returnWaypointButton && config.waypoints && config.waypoints.length > 0) {\n uiElements.returnWaypointButton.addEventListener('click', () => {\n // Find the nearest waypoint based on current camera position\n const cameraPos = camera.getPosition();\n let nearestIndex = 0;\n let nearestDistance = Infinity;\n\n config.waypoints!.forEach((wp, index) => {\n const wpPos = wp.position || [0, 0, 0];\n // Calculate distance (negate Z for coordinate conversion)\n const dx = cameraPos.x - wpPos[0];\n const dy = cameraPos.y - wpPos[1];\n const dz = cameraPos.z - (-wpPos[2]); // Negate Z for Babylon->PlayCanvas\n const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestIndex = index;\n }\n });\n\n console.log('[StorySplat Viewer] Returning to nearest waypoint:', nearestIndex, 'distance:', nearestDistance.toFixed(2));\n\n // Switch to tour mode\n setCameraMode('tour');\n\n // Navigate to the nearest waypoint\n goToWaypoint(nearestIndex);\n\n // Update mode toggle UI buttons\n const modeButtons = uiElements.scrollControls?.querySelectorAll('.storysplat-mode-btn');\n modeButtons?.forEach(btn => {\n const btnMode = btn.getAttribute('data-mode');\n btn.classList.toggle('selected', btnMode === 'tour');\n });\n });\n }\n\n // Emit initial progress to update UI\n events.emit('progressUpdate', { progress: currentProgress, index: currentWaypointIndex });\n\n // Emit initial waypoint change to show first waypoint info\n if (config.waypoints && config.waypoints.length > 0) {\n events.emit('waypointChange', {\n index: 0,\n waypoint: config.waypoints[0],\n prevIndex: -1\n });\n }\n }\n\n // Start autoplay if enabled via options OR via scene config\n if (options.autoPlay || config.autoPlay) {\n play();\n }\n }).catch(err => {\n console.error('[StorySplat Viewer] Failed to initialize:', err);\n // Hide preloader even on error\n if (uiElements.preloader) {\n hidePreloader(uiElements.preloader);\n }\n events.emit('error', err);\n });\n\n // Handle resize\n const handleResize = () => {\n app.resizeCanvas();\n };\n window.addEventListener('resize', handleResize);\n\n // Return viewer instance\n const instance: ViewerInstance = {\n app,\n canvas,\n\n // Navigation\n goToWaypoint,\n nextWaypoint,\n prevWaypoint,\n getCurrentWaypointIndex: () => currentWaypointIndex,\n getWaypointCount: () => config.waypoints?.length || 0,\n\n // Camera\n setPosition: (x, y, z) => camera.setPosition(x, y, z),\n setRotation: (x, y, z) => camera.setEulerAngles(x, y, z),\n getPosition: () => {\n const pos = camera.getPosition();\n return { x: pos.x, y: pos.y, z: pos.z };\n },\n getRotation: () => {\n const rot = camera.getEulerAngles();\n return { x: rot.x, y: rot.y, z: rot.z };\n },\n\n // Playback\n play,\n pause,\n stop,\n isPlaying: () => isPlaying,\n\n // Lifecycle\n destroy: () => {\n // Set destroyed flag first to prevent async operations from completing\n isDestroyed = true;\n pause();\n window.removeEventListener('resize', handleResize);\n // Remove UI elements\n if (uiElements.preloader) uiElements.preloader.remove();\n if (uiElements.scrollControls) uiElements.scrollControls.remove();\n if (uiElements.fullscreenButton) uiElements.fullscreenButton.remove();\n if (uiElements.helpButton) uiElements.helpButton.remove();\n if (uiElements.helpPanel) uiElements.helpPanel.remove();\n if (uiElements.waypointInfo) uiElements.waypointInfo.remove();\n if (uiElements.watermark) uiElements.watermark.remove();\n // Remove injected styles\n const styleEl = document.getElementById('storysplat-viewer-styles');\n if (styleEl) styleEl.remove();\n // Remove container class\n container.classList.remove('storysplat-viewer-container');\n // Cleanup animated GIFs\n animatedGifs.forEach(gif => gif.destroy());\n animatedGifs.length = 0;\n // Cleanup custom meshes\n cleanupCustomMeshes();\n // Cleanup CharacterController\n if (characterController) {\n characterController.destroy();\n }\n // Cleanup Custom Script System\n if ((app as any).__customScriptSystem) {\n (app as any).__customScriptSystem.dispose();\n }\n // Cleanup HTML Mesh Manager\n if ((app as any).__htmlMeshManager) {\n (app as any).__htmlMeshManager.destroy();\n }\n app.destroy();\n canvas.remove();\n },\n resize: handleResize,\n\n // Portal Navigation\n navigateToScene: async (sceneId: string) => {\n const portalData = { targetSceneId: sceneId, activationMode: 'click' };\n await handlePortalNavigation(portalData);\n },\n\n // Events\n on: (event: ViewerEvent, callback) => events.on(event, callback),\n off: (event: ViewerEvent, callback) => events.off(event, callback)\n };\n\n return instance;\n}\n\n/**\n * Create viewer from scene URL\n */\nexport async function createViewerFromUrl(\n container: HTMLElement,\n jsonUrl: string,\n options?: ViewerOptions\n): Promise<ViewerInstance> {\n console.log('[StorySplat Viewer] Fetching scene from:', jsonUrl);\n const response = await fetch(jsonUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch scene: ${response.statusText}`);\n }\n const scene: SceneData = await response.json();\n console.log('[StorySplat Viewer] Scene data loaded:', scene);\n return createViewer(container, scene, options);\n}\n\n// Utility functions\nfunction lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\nfunction lerpAngle(a: number, b: number, t: number): number {\n let diff = b - a;\n while (diff > 180) diff -= 360;\n while (diff < -180) diff += 360;\n return a + diff * t;\n}\n\nfunction easeInOutCubic(t: number): number {\n return t < 0.5\n ? 4 * t * t * t\n : 1 - Math.pow(-2 * t + 2, 3) / 2;\n}\n","/**\n * Create viewer from StorySplat Scene ID\n *\n * This module provides functionality to create a viewer by fetching\n * scene data from the StorySplat API using a scene ID.\n */\n\nimport { createViewer } from './createViewer';\nimport type { ViewerInstance, ViewerOptions, SceneData } from '../types';\n\n/**\n * Options for createViewerFromSceneId\n */\nexport interface ViewerFromSceneIdOptions extends ViewerOptions {\n /**\n * Base URL for the StorySplat API\n * @default 'https://discover.storysplat.com'\n */\n baseUrl?: string;\n\n /**\n * API key for accessing private scenes (future feature)\n */\n apiKey?: string;\n}\n\n/**\n * API response format from /api/scene/{sceneId}\n */\ninterface SceneApiResponse {\n success: boolean;\n data: SceneData;\n meta: {\n sceneId: string;\n name: string;\n description?: string;\n userName?: string; // May be undefined for anonymous/deleted users\n userSlug?: string; // May be undefined for anonymous/deleted users\n thumbnailUrl: string;\n splatUrl: string;\n htmlUrl: string;\n playcanvasHtmlUrl?: string;\n views: number;\n createdAt: string | null;\n };\n}\n\n/**\n * Error thrown when scene fetching fails\n */\nexport class SceneNotFoundError extends Error {\n constructor(sceneId: string) {\n super(`Scene not found: ${sceneId}`);\n this.name = 'SceneNotFoundError';\n }\n}\n\n/**\n * Error thrown when API request fails\n */\nexport class SceneApiError extends Error {\n public statusCode: number;\n\n constructor(message: string, statusCode: number) {\n super(message);\n this.name = 'SceneApiError';\n this.statusCode = statusCode;\n }\n}\n\n/**\n * Get the default API base URL from environment or fallback to production\n */\nfunction getDefaultBaseUrl(): string {\n // Check for environment variable (works in Node.js, bundlers with define, etc.)\n if (typeof process !== 'undefined' && process.env?.STORYSPLAT_API_URL) {\n return process.env.STORYSPLAT_API_URL;\n }\n // Check for window-based config (for browser environments)\n if (typeof window !== 'undefined' && (window as any).__STORYSPLAT_API_URL__) {\n return (window as any).__STORYSPLAT_API_URL__;\n }\n // Default to production\n return 'https://discover.storysplat.com';\n}\n\n/**\n * Default API base URL\n */\nconst DEFAULT_BASE_URL = getDefaultBaseUrl();\n\n/**\n * Create a viewer from a StorySplat scene ID\n *\n * This function fetches scene data from the StorySplat API and creates\n * an embedded viewer. The scene must be public and owned by a user\n * with a public profile.\n *\n * @param container - HTML element to render the viewer in\n * @param sceneId - StorySplat scene ID from your dashboard\n * @param options - Viewer options including API configuration\n * @returns Promise resolving to ViewerInstance\n *\n * @example\n * ```typescript\n * import { createViewerFromSceneId } from 'storysplat-viewer';\n *\n * const viewer = await createViewerFromSceneId(\n * document.getElementById('viewer')!,\n * 'YOUR_SCENE_ID'\n * );\n *\n * // Control playback\n * viewer.play();\n * viewer.pause();\n *\n * // Navigate\n * viewer.nextWaypoint();\n * viewer.goToWaypoint(2);\n *\n * // Listen for events\n * viewer.on('ready', () => console.log('Viewer ready!'));\n * ```\n */\nexport async function createViewerFromSceneId(\n container: HTMLElement,\n sceneId: string,\n options: ViewerFromSceneIdOptions = {}\n): Promise<ViewerInstance> {\n const baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n\n console.log(`[StorySplat Viewer] Fetching scene: ${sceneId}`);\n\n // Build request URL\n const apiUrl = `${baseUrl}/api/scene/${encodeURIComponent(sceneId)}`;\n\n // Build headers\n const headers: HeadersInit = {\n 'Content-Type': 'application/json',\n };\n\n // Add API key if provided (for future private scene support)\n if (options.apiKey) {\n headers['Authorization'] = `Bearer ${options.apiKey}`;\n }\n\n // Fetch scene data\n const response = await fetch(apiUrl, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new SceneNotFoundError(sceneId);\n }\n\n const errorText = await response.text();\n let errorMessage: string;\n\n try {\n const errorJson = JSON.parse(errorText);\n errorMessage = errorJson.error || `API error: ${response.status}`;\n } catch {\n errorMessage = `API error: ${response.status}`;\n }\n\n throw new SceneApiError(errorMessage, response.status);\n }\n\n const apiResponse: SceneApiResponse = await response.json();\n\n if (!apiResponse.success || !apiResponse.data) {\n throw new SceneApiError('Invalid API response format', 500);\n }\n\n console.log(`[StorySplat Viewer] Scene loaded: \"${apiResponse.meta.name}\"`);\n\n // Merge scene metadata into scene data if not present\n const sceneData: SceneData = {\n ...apiResponse.data,\n // Ensure these are set from metadata if not in scene data\n name: apiResponse.data.name || apiResponse.meta.name,\n thumbnailUrl: apiResponse.data.thumbnailUrl || apiResponse.meta.thumbnailUrl,\n };\n\n // Extract viewer-specific options (remove API-specific ones)\n const { baseUrl: _baseUrl, apiKey: _apiKey, ...viewerOptions } = options;\n\n // Create and return the viewer\n return createViewer(container, sceneData, viewerOptions);\n}\n\n/**\n * Fetch scene metadata without creating a viewer\n *\n * Useful for displaying scene information before loading the full viewer.\n *\n * @param sceneId - StorySplat scene ID\n * @param options - API configuration options\n * @returns Promise resolving to scene metadata\n *\n * @example\n * ```typescript\n * import { fetchSceneMeta } from 'storysplat-viewer';\n *\n * const meta = await fetchSceneMeta('YOUR_SCENE_ID');\n * console.log(`Scene: ${meta.name} by ${meta.userName}`);\n * console.log(`Views: ${meta.views}`);\n * ```\n */\nexport async function fetchSceneMeta(\n sceneId: string,\n options: { baseUrl?: string; apiKey?: string } = {}\n): Promise<{\n name: string;\n description: string;\n thumbnailUrl: string;\n userName: string; // Defaults to 'Unknown' if not available\n userSlug: string; // Defaults to 'unknown' if not available\n views: number;\n tags: string[];\n category?: string;\n createdAt: string | null;\n}> {\n const baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n const apiUrl = `${baseUrl}/api/scene/${encodeURIComponent(sceneId)}/meta`;\n\n const headers: HeadersInit = {\n 'Content-Type': 'application/json',\n };\n\n if (options.apiKey) {\n headers['Authorization'] = `Bearer ${options.apiKey}`;\n }\n\n const response = await fetch(apiUrl, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new SceneNotFoundError(sceneId);\n }\n throw new SceneApiError(`API error: ${response.status}`, response.status);\n }\n\n const data = await response.json();\n\n // Ensure userName and userSlug have fallback values\n return {\n ...data,\n userName: data.userName || 'Unknown',\n userSlug: data.userSlug || 'unknown',\n };\n}\n"],"names":["escapeHtml","str","replace","generateHTML","sceneData","options","cdnUrl","title","name","description","faviconUrl","customCSS","faviconTag","customCSSBlock","rawJSON","JSON","stringify","key","value","HTMLElement","sceneDataJSON","async","generateHTMLFromUrl","jsonUrl","response","fetch","ok","Error","statusText","json","transformSceneToExportProps","scene","originalUrl","loadedModelUrl","splatUrl","sogUrl","sogModelUrl","compressedPlyUrl","lodMetaUrl","originalExt","split","pop","toLowerCase","isPlayCanvasCompatible","includes","console","warn","log","original","selected","waypoints","map","wp","fov","Math","PI","info","interactions","infoInteraction","find","i","type","data","text","position","x","y","z","rotation","duration","triggerDistance","sceneId","userId","userName","thumbnailUrl","fallbackUrls","filter","url","scale","splatScale","splatPosition","cameraPosition","splatRotation","cameraRotation","invertXScale","invertYScale","hotspots","portals","skybox","customMeshes","htmlMeshes","lights","particles","collisionMeshesData","playerHeight","uiColor","uiOptions","showStartExperience","showWatermark","hideFullscreenButton","hideInfoButton","hideMuteButton","hideHelpButton","hideWatermark","watermarkText","watermarkLink","buttonPosition","buttonLabels","customPreloaderLogoUrl","defaultCameraMode","allowedCameraModes","cameraMovementSpeed","cameraRotationSensitivity","includeScrollControls","scrollButtonMode","scrollAmount","scrollSpeed","autoPlayEnabled","autoplaySpeed","loopMode","includeXR","xrMode","customScript","templateType","additionalSplats","keepMeshesInMemory","exportPropsToViewerConfig","props","cameraMode","autoPlay","nearClip","minClipPlane","farClip","maxClipPlane","DEFAULT_BUTTON_LABELS","tour","explore","hybrid","walk","orbit","next","previous","startExperience","returnToTour","fullscreen","mute","unmute","helpTitle","percentageFormat","getButtonLabel","labels","injectStyles","existingStyle","document","getElementById","remove","style","createElement","id","textContent","generateViewerStyles","head","appendChild","createPreloader","container","customLogoUrl","preloader","className","useCustomLogo","innerHTML","Promise","resolve","customElements","get","existingScript","querySelector","addEventListener","script","src","onload","hidePreloader","classList","add","setTimeout","connectUIToViewer","elements","viewer","prevBtn","scrollControls","nextBtn","playBtn","prevWaypoint","nextWaypoint","updatePlayButtonIcon","isPlaying","pause","play","on","helpButton","helpPanel","toggle","fullscreenButton","test","navigator","userAgent","platform","maxTouchPoints","display","parentElement","doc","fullscreenElement","webkitFullscreenElement","exitFullscreen","webkitExitFullscreen","expandIcon","compressIcon","requestFullscreen","webkitRequestFullscreen","handleFullscreenChange","isFullscreen","setTourControlsVisible","visible","progressText","progressContainer","scrollButtons","setCameraMode","modeButtons","querySelectorAll","forEach","btn","mode","getAttribute","b","btnMode","lastProgressUpdate","lastDisplayedPercentage","progress","percentage","max","min","roundedPercentage","round","now","performance","progressBar","width","String","lastDisplayedWaypointIndex","index","waypoint","waypointInfo","titleEl","descEl","getWaypoints","showHotspotPopup","hotspot","popup","contentEl","closeBtn","cssText","activationMode","hasMediaContent","photoUrl","popupVideoUrl","contentType","iframeUrl","backgroundColor","textColor","color","fontFamily","fontSize","closeButtonColor","contentHtml","information","externalLinkUrl","btnColor","externalLinkButtonColor","externalLinkText","setJoystickVisible","joystick","lookZone","updateJoystickPosition","active","dx","dy","maxRadius","joystickThumb","distance","sqrt","clampedDistance","clampedX","clampedY","transform","setCameraModeToggleVisible","cameraModeToggle","updateCameraModeToggle","setReturnWaypointButtonVisible","returnWaypointButton","tmpV1","pc","Vec3","tmpV2","pose","Pose","frame","InputFrame","move","rotate","applyDeadZone","stick","low","high","mag","fill","screenToWorld","camera","dz","out","aspectRatio","horizontalFov","projection","orthoHeight","app","system","height","graphicsDevice","clientRect","set","halfSize","PROJECTION_PERSPECTIVE","halfSlice","tan","math","DEG_TO_RAD","mul","CameraControls","constructor","config","this","enabled","_mode","_enableOrbit","_enableFly","enablePan","_pose","_startZoomDist","_pitchRange","Vec2","_yawRange","Infinity","_lastFocusPoint","_zoomRange","_state","axis","shift","ctrl","mouse","touches","moveSpeed","moveFastSpeed","moveSlowSpeed","rotateSpeed","rotateTouchSens","rotateJoystickSens","zoomSpeed","zoomPinchSens","gamepadDeadZone","joystickEventName","_destroyHandler","cameraComponent","_flyController","FlyController","_orbitController","OrbitController","_focusController","FocusController","zoomRange","canvas","_desktopInput","KeyboardMouseSource","_orbitMobileInput","MultiTouchSource","_flyMobileInput","DualGestureSource","_gamepadInput","GamepadSource","attach","bx","by","sx","sy","fire","look","getPosition","ZERO","_setMode","_controller","undefined","enableOrbit","enableFly","focusPoint","focusDamping","rotateDamping","moveDamping","zoomDamping","pitchRange","yawRange","mobileInputLayout","enable","damping","point","getFocus","range","copy","clamp","layout","previousMode","detach","_lastYaw","angles","setMode","focus","resetZoom","zoomDist","forward","mulScalar","sub","normalize","reset","syncFromCamera","target","clone","focusDistance","yaw","eulerAngles","getEulerAngles","syncFromPose","focusTarget","setPosition","setRotation","tempEntity","Entity","disable","read","update","dt","keyCode","button","wheel","touch","pinch","count","leftInput","rightInput","leftStick","rightStick","D","A","RIGHT","LEFT","E","Q","W","S","UP","DOWN","length","SHIFT","CTRL","fly","double","desktopPan","mobileJoystick","endsWith","moveMult","zoomMult","zoomTouchMult","rotateMult","rotateTouchMult","rotateJoystickMult","deltas","v","keyMove","panMove","wheelMove","append","mouseRotate","flyMove","orbitMove","pinchMove","orbitRotate","flyRotate","stickMove","stickRotate","xr","focusInterrupt","focusComplete","complete","yawDelta","setEulerAngles","destroy","CharacterController","velocity","isGrounded","pitch","keys","mouseLocked","sprintMultiplier","lookSensitivity","gravity","maxFallSpeed","jumpVelocity","collisionRadius","stepHeight","groundCheckDistance","collisionEntities","floorEntity","keydownHandler","keyupHandler","mousemoveHandler","clickHandler","pointerlockchangeHandler","tmpVec","tmpVec2","right","createCollisionMeshes","loadPromises","meshType","customMeshUrl","promise","loadCustomCollisionMesh","push","entity","addComponent","configureCollisionEntity","all","ext","assetType","asset","Asset","reject","ready","containerResource","resource","instantiateRenderEntity","renderEntity","children","addChild","computeAndStoreBounds","err","error","assets","load","bounds","BoundingBox","boundsInitialized","traverse","node","render","meshInstances","mi","aabb","child","_collisionBounds","halfExtents","pos","rot","scaling","setLocalScale","setEntityVisibility","_collisionMeshType","root","setupInputHandlers","removeInputHandlers","pointerLockElement","exitPointerLock","e","code","movementX","movementY","requestPointerLock","removeEventListener","checkCollision","radius","entityPos","entityScale","getLocalScale","halfWidth","halfHeight","halfDepth","customBounds","abs","checkGround","floorPos","floorScale","highestGround","topY","moveX","moveZ","isSprinting","yawRad","sin","cos","speed","currentPos","newPos","groundY","targetFeetY","grounded","getVelocity","_GsplatRevealRadialClass","getGsplatRevealRadialClass","GsplatRevealRadial","createScript","Object","assign","prototype","effectTime","_materialsApplied","_shadersApplied","_retryCount","_maxRetries","_materialCreatedHandler","_systemMaterialHandler","_centerArray","_dotTintArray","_waveTintArray","center","acceleration","delay","dotTint","waveTint","oscillationIntensity","endRadius","initialize","Set","Color","_applyShaders","_removeShaders","floor","toFixed","size","_isEffectComplete","_updateUniforms","_setUniform","r","g","_getCompletionTime","liftStartTime","discriminant","getShaderGLSL","getShaderWGSL","gsplatComponent","gsplat","isUnified","unified","gsplatSystem","systems","material","layer","_applyShaderToMaterial","cameras","findComponents","layers","cameraComp","layerId","getLayerById","getGSplatMaterial","instance","_instance","_applyToInstance","layerList","gsplatInstance","_gsplatInstance","gsplatInst","checkEntity","inst","materials","_materials","Map","Array","isArray","_material","has","methods","obj","names","getOwnPropertyNames","getPrototypeOf","m","startsWith","join","glsl","wgsl","hasSetShaderChunk","setShaderChunk","chunks","gsplatEffectGLSL","gsplatEffectWGSL","shader","setParameter","clear","off","REVEAL_PRESETS","fast","medium","slow","getRevealPreset","preset","defineProperty","lib","loop","conditional","parse","stream","schema","result","arguments","parent","partSchema","conditionFunc","continueFunc","arr","lastStreamPos","newParent","uint8","readBits","readArray","readString","peekBytes","readBytes","readByte","buildStream","uint8Data","peekByte","offset","subarray","from","fromCharCode","readUnsigned","littleEndian","bytes","byteSize","totalOrFunc","total","parser","_byte","bits","reduce","res","def","startIndex","pow","subBitsTotal","decompressFrames","decompressFrame","parseGIF","_gif","exports","_","require$$0","_uint","require$$1","subBlocksSchema","blocks","streamSize","availableSize","Uint8Array","gceSchema","gce","codes","extras","future","disposal","userInput","transparentColorGiven","transparentColorIndex","terminator","imageSchema","image","descriptor","left","top","lct","exists","interlaced","sort","minCodeSize","textSchema","blockSize","preData","applicationSchema","application","commentSchema","comment","_default","header","signature","version","lsd","gct","resolution","backgroundColorIndex","pixelAspectRatio","frames","nextCode","__esModule","default","_jsBinarySchemaParser","require$$2","_deinterlace","deinterlace_1","deinterlace","pixels","newPixels","rows","cpRow","toRow","fromRow","fromPixels","slice","splice","apply","concat","offsets","steps","pass","_lzw","lzw_1","lzw","pixelCount","available","code_mask","code_size","end_of_information","in_code","old_code","data_size","datum","first","pi","bi","MAX_STACK_SIZE","npix","dstPixels","prefix","suffix","pixelStack","arrayBuffer","byteData","buildImagePatch","totalPixels","resultImage","dims","colorTable","disposalType","transparentIndex","patch","patchData","Uint8ClampedArray","colorIndex","generatePatch","parsedGif","buildImagePatches","f","AnimatedGifTexture","currentFrameIndex","isLoaded","lastFrameTime","updateHandler","texture","gifWidth","gifHeight","drawFrame","ctx","getContext","willReadFrequently","buffer","gif","Texture","format","PIXELFORMAT_RGBA8","mipmaps","minFilter","FILTER_LINEAR","magFilter","addressU","ADDRESS_CLAMP_TO_EDGE","addressV","onReady","onError","frameIndex","prevFrame","clearRect","imageData","ImageData","tempCanvas","putImageData","drawImage","updateTexture","getImageData","lock","unlock","upload","stop","playing","loaded","HtmlMeshManager","meshes","useTexElement2D","GraphicsDevice","_isHTMLElementInterface","HTMLImageElement","HTMLCanvasElement","HTMLVideoElement","originalIsBrowserInterface","_isBrowserInterface","call","patchPlayCanvasForHtmlMesh","device","supportsTexElement2D","createMesh","htmlElement","createHtmlElement","createTexture","createMaterial","createEntity","destroyMesh","updateMeshTexture","animated","startUpdateLoop","pointerEvents","zIndex","overflow","css","html","setAttribute","visibility","body","setSource","renderToCanvas","svg","img","Image","blob","Blob","URL","createObjectURL","revokeObjectURL","onerror","fillStyle","fillRect","font","textAlign","fillText","StandardMaterial","diffuseMap","emissiveMap","emissive","opacity","blendType","BLEND_NORMAL","BLEND_NONE","cull","doubleSided","CULLFACE_NONE","CULLFACE_BACK","aspect","castShadows","receiveShadows","billboard","findComponent","lookAt","lastUpdate","rate","updateRate","last","delete","values","some","getMesh","updateVisibility","scrollPercent","waypointIndex","visibilityRange","start","end","billboardRange","billboardActive","_billboardActive","getAllMeshes","CustomScriptSystem","isInitialized","scriptCleanup","lastError","updateCallbacks","api","registerCleanup","fn","addCleanup","updateScript","execute","sanitizeScript","s","preprocessScript","processed","trim","cleanup","processedScript","cancelled","watchdog","requestAnimationFrame","wrappedApp","create","registerUpdate","callback","safeCallback","idx","indexOf","registerBeforeRender","pcNamespace","getScrollPercentage","getCurrentWaypointIndex","getHotspots","getSplats","getHTMLMeshes","func","Function","module","fakeRequire","blocked","Proxy","cleanupCandidate","bind","getLastError","dispose","EventEmitter","listeners","event","emit","args","cb","isMobileDevice","vendor","window","opera","LOD_PRESETS","lodDistances","desktop","mobile","isLodStreamingFormat","createViewer","lazyLoad","lazyEvents","actualInstance","lazyLoadThumbnail","buttonText","lazyLoadButtonText","onStart","lazyLoadContainer","startBtn","transition","createLazyLoadUI","goToWaypoint","getWaypointCount","getRotation","el","resize","navigateToScene","events","showUI","uiOpts","mapCameraMode","hasCollisionMeshesForWalk","allowedModes","a","defaultMode","uiElements","showScrollControls","showModeToggle","showFullscreenButton","showHelpButton","showPreloader","fullscreenBtn","vrBtn","vrButton","arBtn","arButton","helpBtn","hotspotPopup","watermark","finalLink","createUIElements","baseGraphicsOptions","antialias","alpha","powerPreference","Application","graphicsDeviceOptions","Mouse","TouchDevice","keyboard","Keyboard","webgl2Error","preferWebGl2","webgl1Error","errorDiv","heading","message","preventDefault","setCanvasFillMode","FILLMODE_FILL_WINDOW","setCanvasResolution","RESOLUTION_AUTO","isMobile","lodPresetName","lodPreset","lodUpdateAngle","lodBehindPenalty","radialSorting","lodUpdateDistance","lodUnderfillLimit","lodRangeMin","lodRangeMax","colorUpdateDistance","colorUpdateAngle","colorUpdateDistanceLodScale","colorUpdateAngleLodScale","lodRange","currentWaypointIndex","splatEntity","revealScript","isDestroyed","currentSplatUrl","isLoadingSplat","preloadedSplats","lastSplatCheckProgress","lastSplatCheckWaypointIndex","clearColor","hex","parseInt","substring","finalRot","convertWaypointRotation","light","LIGHTTYPE_DIRECTIONAL","intensity","cameraControls","characterController","then","catch","currentCameraMode","waypointControlEnabled","picker","Picker","isInXR","xrSessionType","userYawOffset","userPitchOffset","isUserDragging","targetCameraPosition","targetCameraRotation","pickerScale","canvasEl","clientWidth","clientHeight","worldLayer","getLayerByName","prepare","centerX","centerY","worldPoint","getWorldPointAsync","findBetterFocusPoint","cameraPos","hotspotEntities","hotspotData","videoElement","mediaTriggerMode","hotspotPos","proximityDistance","isVideoPlaying","playVideoHotspot","pauseVideoHotspot","updateProximityTriggers","waypointAudioMap","audioData","audioId","slotId","assetReady","slot","sound","spatialSound","audioPos","maxDistance","stopOnExit","updateWaypointAudioProximity","wpPos","triggerDist","activeWaypointTriggers","interaction","executeWaypointInteractions","reverseWaypointInteractions","checkWaypointTriggerDistance","currentMode","updateProgress","bar","textEl","percent","updatePreloaderProgress","qx","_x","qy","_y","qz","_z","qw","_w","w","Quat","setFromEulerAngles","hideSplat","disposeSplat","preloadNextSplat","currentIndex","nextIndex","nextSplat","Date","invertX","invertY","finalScale","finalRotDeg","preloadSplat","loadSwapSplat","currentEntity","showSplat","swapIndex","findIndex","updateSplats","numWaypoints","currentProgress","bestSplat","bestTrigger","splat","primaryUrl","targetUrl","skyboxUrl","skyboxAsset","TEXTURETYPE_RGBM","skyboxMip","rotQuat","skyboxRotation","applySkybox","totalDuration","sum","playbackSpeed","rawLoopMode","playbackDirection","lastPointerX","lastPointerY","waypointPositions","waypointRotations","waypointFOVs","defaultFOV","targetFOV","updateCameraFromProgress","segmentProgress","segmentIndex","t","startPos","endPos","lerp","startRot","endRot","slerp","startFOV","endFOV","newIndex","prevIndex","currentWaypoint","waypointCameraMode","orbitTarget","previousIndex","autoplayTriggered","isLeaving","autoplay","setProgress","animate","animateToProgress","targetProgress","startProgress","startTime","elapsed","newFOV","userRotationOffset","targetWithUserOffset","mul2","currentRot","newRot","lastPlaybackTime","playbackAnimationId","playbackLoop","timestamp","deltaTime","cancelAnimationFrame","scrollCanvas","deltaY","passive","clientX","clientY","capture","deltaX","portalEntities","parseColor","allAudioContexts","particleEntities","particleTextures","PARTICLE_TEXTURE_URLS","flare","circle","spark","rain","smoke","loadParticleTexture","createParticleSystemEntity","psConfig","getPos","vec","defaultVal","getColor","emitterPos","emitterPosition","color1","color2","colorDead","direction1","direction2","lifetime","minLifeTime","maxLifeTime","emitterShape","EMITTERSHAPE_BOX","emitterExtents","emitterOffset","emitterType","EMITTERSHAPE_SPHERE","emitterRadius","emitBoxMin","emitBoxMax","boxMin","boxMax","blendMode","BLEND_ADDITIVEALPHA","blendModeStr","BLEND_MULTIPLICATIVE","BLEND_ADDITIVE","gravityX","gravityY","gravityZ","endVelX","endVelY","endVelZ","minAngularSpeed","maxAngularSpeed","angularSpeed","minInitialRotation","maxInitialRotation","minSize","maxSize","minScaleX","maxScaleX","minScaleY","maxScaleY","minEmitPower","maxEmitPower","avgDirX","avgDirY","avgDirZ","numParticles","emitRate","startAngle","startAngle2","radialSpeedGraph","Curve","localVelocityGraph","CurveSet","localVelocityGraph2","velocityGraph","scaleGraph","scaleGraph2","rotationSpeedGraph","rotationSpeedGraph2","colorGraph","alphaGraph","blend","depthWrite","depthSoftening","softParticles","lighting","halfLambert","alignToMotion","stretch","preWarm","orientation","particlesystem","localSpace","customMeshEntities","playAnimComponentV2","animInfo","component","animations","clips","modelEntity","animList","firstAnim","_name","_duration","anim","stateGraphData","states","transitions","to","time","conditions","activate","loadStateGraph","animAsset","assignAnimation","animComponent","loopedTime","_curves","curve","paths","_paths","entityPath","findByPath","findByName","evaluate","prop","propertyPath","setLocalPosition","setLocalRotation","baseLayer","playableState","pauseAnimComponentV2","loadCustomMesh","meshConfig","modelUrl","radToDeg","modelAsset","meshId","meshData","isAnimPlaying","audioPlaying","enableAllChildren","childCount","opacityMode","applyOpacityToCustomMesh","originalRotation","camPos","meshPos","angle","atan2","hasGlbAnimations","hasAnimations","animationCount","interactionConfig","playModelAnimation","allAnimComponents","findAllAnimComponents","depth","animation","animationNames","currentTime","shouldAutoPlay","animationAutoPlay","playAudio","audioUrl","audioConfig","positional","audioSpatial","distanceModel","audioDistanceModel","refDistance","audioRefDistance","audioMaxDistance","rollOffFactor","audioRollOffFactor","slots","audioLoop","volume","audioVolume","audioSlotId","audioAsset","setupMeshAudio","customMesh","triggerUIPopup","triggerDirectLink","hasInteractions","canvasForMesh","performRaycast","rect","getBoundingClientRect","dir","sub2","rayIntersectsMeshHierarchy","dot","add2","popupTriggerMode","audioTriggerMode","animationTriggerMode","directLinkTriggerMode","isHovering","handleHoverIn","cursor","displayMeshContent","handleDirectLink","handleHoverOut","meshPopup","hideMeshContent","handleClick","meshClickHandler","meshHoverHandler","nowHovering","meshLeaveHandler","setupMeshClickInteraction","opacityConfig","rayOrigin","rayDir","rayIntersectsAABB","model","getMin","getMax","tmin","tmax","origin","minVal","maxVal","t1","t2","existingPopup","popupContent","content","onclick","showMeshPopup","directLinkUrl","open","applyToEntity","ent","meshInstance","_isCloned","cleanupCustomMeshes","canvasForCleanup","hexToColorForLights","exec","initCustomLights","lightConfig","LIGHTTYPE_POINT","createPointLight","createDirectionalLightEntity","groundColor","skyColor","ambientLight","createHemisphericLight","createAmbientLight","angleDeg","exponent","innerAngleDeg","innerConeAngle","innerAngle","outerAngleDeg","outerConeAngle","outerAngle","LIGHTTYPE_SPOT","shadowBias","normalOffsetBias","direction","dirVec","createSpotLight","getPlayCanvasDistanceModel","modelStr","opacityAnimation","startPercent","endPercent","startOpacity","endOpacity","bRange","updateCustomMeshVisibility","animatedGifs","createImageMaterial","imageUrl","useLighting","onTextureReady","BLEND_PREMULTIPLIED","depthTest","twoSidedLighting","alphaTest","diffuse","specular","lower","isGifUrl","gifTexture","opacityMap","crossOrigin","FILTER_LINEAR_MIPMAP_LINEAR","createHotspots","sz","rotX","rotY","rotZ","sphereOpacity","initialOpacity","targetOpacity","textureLoaded","hiddenUntilTextureLoaded","shouldBeVisible","rotateLocal","hotspotMaterial","videoUrl","useAlphaMethod","useIOSVideoAlphaMethod","forceIOSVideoAlphaMethodForAllDevices","mainVideoUrl","iosMainVideoUrl","alphaVideoUrl","alphaMaskVideoUrl","hasWebMAlpha","isWebMUrl","webmHasAlpha","createVideoAndTexture","isAlpha","useRGBA","videoLoop","playsInline","muted","videoMuted","PIXELFORMAT_R8_G8_B8_A8","PIXELFORMAT_R8_G8_B8","video","main","videoTexture","alphaVideo","alphaTexture","opacityMapChannel","paused","readyState","HAVE_ENOUGH_DATA","initialScale","videoWidth","videoHeight","ratio","alphaVideoElement","videoSpatialAudio","audioCtx","AudioContext","webkitAudioContext","source","createMediaElementSource","panner","createPanner","panningModel","videoDistanceModel","videoRefDistance","videoMaxDistance","rolloffFactor","videoRolloffFactor","connect","destination","camForward","camUp","up","listener","positionX","positionY","positionZ","forwardX","forwardY","forwardZ","upX","upY","upZ","setOrientation","setupVideoSpatialAudio","gifUrl","loadAnimatedGif","gifCanvas","animateGif","gifAnimationFrame","fallbackMaterial","audioElements","audio","audioRolloffFactor","updateAudioPosition","setupHotspotAudio","hasBillboardRange","billboardRangeStart","billboardRangeEnd","_billboardOriginalRotation","updateHotspotVisibility","rangeStart","rangeEnd","origRot","triggerMode","state","resume","updatePortalVisibility","portal","portalData","portalPos","proximityTriggered","targetSceneName","targetSceneId","handlePortalNavigation","raycastPortals","closestHit","portalId","sceneName","existing","loadingDiv","spinner","background","flexDirection","alignItems","justifyContent","border","borderTop","borderRadius","marginBottom","styleEl","showPortalLoadingUI","sceneApiUrl","status","newSceneData","__htmlMeshManager","__customScriptSystem","cleanupForPortalNavigation","parentNode","newViewer","hidePortalLoadingUI","padding","raycastHotspots","currentHoverHotspot","isMouseOverPopup","overlay","handleDoubleClickFocus","screenX","screenY","hotspotHit","scaledX","scaledY","getSelectionAsync","aabbCenter","portalHit","effectiveActivationMode","hit","popupEl","overlayEl","hitResult","hasPopupContent","teleportWaypoint","teleportToWaypoint","teleportPercent","teleportToPercent","teleportMode","calculatedTargetProgress","targetIndex","lastTapTime","changedTouches","urls","received","hasRejected","unhandledRejectionHandler","errorMsg","reason","details","gs","hasUserRotation","revealPresetName","revealEffect","revealConfig","GsplatRevealRadialClass","syncError","statusCode","stack","loadSplat","portalMaterial","audioEntity","soundConfig","audioDataRef","ps","textureName","particleTexture","textureUrl","textureCacheKey","customTextureUrl","colorMap","safeName","initParticleSystems","loadedCount","skippedCount","hasModelUrl","initCustomMeshes","skyboxIntensity","enableIBL","isHDR","skyboxEntity","skyboxMaterial","CULLFACE_FRONT","textureAsset","exposure","toneMapping","TONEMAP_ACES","iblError","rotationDegrees","initSkybox","isAvailable","XRTYPE_VR","XRTYPE_AR","startXr","XRSPACE_LOCALFLOOR","setupXR","htmlMeshManager","manager","setupHtmlMeshes","customScriptSystem","scriptSystem","setupCustomScript","urlParams","URLSearchParams","location","search","waypointParam","autoplayParam","isNaN","nearestIndex","nearestDistance","handleResize","resizeCanvas","createViewerFromUrl","SceneNotFoundError","super","SceneApiError","DEFAULT_BASE_URL","process","env","STORYSPLAT_API_URL","__STORYSPLAT_API_URL__","createViewerFromSceneId","baseUrl","apiUrl","encodeURIComponent","headers","apiKey","method","errorText","errorMessage","apiResponse","success","meta","_baseUrl","_apiKey","viewerOptions","fetchSceneMeta","userSlug"],"mappings":"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","x_google_ignoreList":[7,8,9,10,11,12]}
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/html-generation/generateHTML.ts","../src/transformers/sceneToConfig.ts","../src/dynamic-viewer/viewerUI.ts","../src/dynamic-viewer/CameraControls.ts","../src/dynamic-viewer/CharacterController.ts","../src/effects/gsplat-reveal-radial.ts","../src/effects/reveal-presets.ts","../node_modules/js-binary-schema-parser/lib/index.js","../node_modules/js-binary-schema-parser/lib/parsers/uint8.js","../node_modules/gifuct-js/lib/index.js","../node_modules/js-binary-schema-parser/lib/schemas/gif.js","../node_modules/gifuct-js/lib/deinterlace.js","../node_modules/gifuct-js/lib/lzw.js","../src/dynamic-viewer/AnimatedGifHelper.ts","../src/dynamic-viewer/HtmlMeshHelper.ts","../src/dynamic-viewer/CustomScriptSystem.ts","../src/dynamic-viewer/createViewer.ts","../src/dynamic-viewer/createViewerFromSceneId.ts"],"sourcesContent":["/**\n * Simple HTML Generator for StorySplat\n *\n * Generates standalone HTML that loads the viewer from CDN.\n * This replaces the old 13K+ line HTML generation system with a simple\n * bootstrap HTML that uses the same viewer code as dynamic embedding.\n */\n\nimport type { SceneData } from '../types';\n\nexport interface GenerateHTMLOptions {\n /** Custom CDN URL for the viewer bundle. Default: unpkg */\n cdnUrl?: string;\n /** HTML page title. Default: scene name */\n title?: string;\n /** Meta description for SEO */\n description?: string;\n /** Favicon URL */\n faviconUrl?: string;\n /** Custom CSS to inject */\n customCSS?: string;\n /** Whether to minify the output */\n minify?: boolean;\n}\n\n/**\n * Escape HTML entities in a string\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Escape string for safe embedding in inline <script> tags.\n * Prevents XSS by escaping </script> sequences that would close the script tag prematurely.\n */\nfunction escapeForInlineScript(str: string): string {\n // Escape </script> (case-insensitive) to prevent script tag injection\n // Using Unicode escapes for the < character within script context\n return str.replace(/<\\/script/gi, '<\\\\/script');\n}\n\n/**\n * Generate standalone HTML for a StorySplat scene\n *\n * @param sceneData - Scene data object (same format as createViewer)\n * @param options - Generation options\n * @returns Complete HTML string ready to be saved as .html file\n *\n * @example\n * ```typescript\n * import { generateHTML } from 'storysplat-viewer';\n *\n * const html = generateHTML(sceneData, { title: 'My Scene' });\n * // Save html to file or serve it\n * ```\n */\nexport function generateHTML(sceneData: SceneData, options: GenerateHTMLOptions = {}): string {\n const {\n cdnUrl = 'https://unpkg.com/storysplat-viewer@2/dist/storysplat-viewer.umd.js',\n title = sceneData.name || 'StorySplat Scene',\n description = `Interactive 3D scene: ${sceneData.name || 'StorySplat'}`,\n faviconUrl,\n customCSS = ''\n } = options;\n\n // PlayCanvas CDN URL - loaded before viewer bundle (UMD expects 'pc' global)\n const playcanvasCdnUrl = 'https://cdn.jsdelivr.net/npm/playcanvas@2.14.3/build/playcanvas.min.js';\n\n const faviconTag = faviconUrl\n ? `<link rel=\"icon\" href=\"${escapeHtml(faviconUrl)}\" />`\n : '';\n\n const customCSSBlock = customCSS\n ? `<style>${customCSS}</style>`\n : '';\n\n // Serialize scene data, handling circular references\n // Then escape for safe embedding in inline <script> tags\n const rawJSON = JSON.stringify(sceneData, (key, value) => {\n // Skip any DOM nodes or circular refs\n // Check for HTMLElement safely (it doesn't exist in Node.js)\n if (typeof HTMLElement !== 'undefined' && value instanceof HTMLElement) return undefined;\n if (typeof value === 'function') return undefined;\n // Skip any object with nodeType (DOM nodes)\n if (value && typeof value === 'object' && 'nodeType' in value) return undefined;\n return value;\n }, 2);\n // Escape </script> sequences to prevent XSS attacks\n const sceneDataJSON = escapeForInlineScript(rawJSON);\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n <meta name=\"description\" content=\"${escapeHtml(description)}\">\n <title>${escapeHtml(title)}</title>\n ${faviconTag}\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body {\n width: 100%;\n /* Use 100dvh for iOS address bar support, with 100vh fallback */\n height: 100vh;\n height: 100dvh;\n overflow: hidden;\n background: #111;\n }\n #app {\n width: 100%;\n /* Use 100dvh for iOS address bar support, with 100vh fallback */\n height: 100vh;\n height: 100dvh;\n position: relative;\n }\n /* Loading state */\n #app:empty::after {\n content: 'Loading...';\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n color: #666;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n font-size: 16px;\n }\n </style>\n ${customCSSBlock}\n</head>\n<body>\n <div id=\"app\"></div>\n\n <script src=\"${playcanvasCdnUrl}\"></script>\n <script src=\"${escapeHtml(cdnUrl)}\"></script>\n <script>\n (function() {\n 'use strict';\n\n var sceneData = ${sceneDataJSON};\n\n // Wait for DOM and viewer to be ready\n function init() {\n var container = document.getElementById('app');\n if (!container) {\n console.error('[StorySplat] Container #app not found');\n return;\n }\n\n if (typeof StorySplatViewer === 'undefined' || !StorySplatViewer.createViewer) {\n console.error('[StorySplat] Viewer not loaded. Check CDN URL.');\n return;\n }\n\n try {\n var viewer = StorySplatViewer.createViewer(container, sceneData, {\n showUI: true,\n autoPlay: false\n });\n\n viewer.on('ready', function() {\n console.log('[StorySplat] Scene ready');\n });\n\n viewer.on('error', function(err) {\n console.error('[StorySplat] Error:', err);\n });\n\n // Expose viewer globally for debugging\n window.storySplatViewer = viewer;\n } catch (err) {\n console.error('[StorySplat] Failed to create viewer:', err);\n }\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n } else {\n init();\n }\n })();\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Generate HTML from a scene URL (fetches the JSON first)\n *\n * @param jsonUrl - URL to fetch scene JSON from\n * @param options - Generation options\n * @returns Complete HTML string\n */\nexport async function generateHTMLFromUrl(\n jsonUrl: string,\n options: GenerateHTMLOptions = {}\n): Promise<string> {\n const response = await fetch(jsonUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch scene: ${response.statusText}`);\n }\n const sceneData: SceneData = await response.json();\n return generateHTML(sceneData, options);\n}\n","/**\n * Scene Data to ExportProps Transformer\n *\n * Converts Firebase scene JSON or direct scene data to the ExportProps format\n * used by the HTML generation system.\n */\n\nimport type { SceneData, WaypointData, ExportProps, SplatSwapPoint } from '../types';\n\n/**\n * Transform scene data to ExportProps format\n */\nexport function transformSceneToExportProps(scene: SceneData): ExportProps {\n // Get all available URLs\n const originalUrl = scene.loadedModelUrl || scene.splatUrl || '';\n const sogUrl = scene.sogModelUrl || scene.sogUrl;\n const compressedPlyUrl = scene.compressedPlyUrl;\n const lodMetaUrl = scene.lodMetaUrl; // LOD streaming format (highest priority)\n\n // For PlayCanvas, prefer formats in this order: LOD > SOG > Compressed PLY > PLY > Original\n // PlayCanvas can't directly load .splat files, so we need PLY-compatible formats\n const originalExt = originalUrl.split('?')[0].split('.').pop()?.toLowerCase();\n const isPlayCanvasCompatible = originalExt === 'ply' || originalUrl.includes('.compressed.ply');\n\n // Choose the best URL for PlayCanvas (for backward compatibility, splatUrl is the main URL)\n // LOD streaming is handled separately as lodMetaUrl\n let splatUrl = originalUrl;\n if (sogUrl) {\n splatUrl = sogUrl; // SOG is best (after LOD)\n } else if (compressedPlyUrl) {\n splatUrl = compressedPlyUrl; // Compressed PLY is second best\n } else if (!isPlayCanvasCompatible) {\n // Original is not compatible, keep it but warn\n console.warn('[StorySplat Viewer] Original file format may not be compatible with PlayCanvas:', originalExt);\n }\n\n console.log('[StorySplat Viewer] URL selection:', {\n original: originalUrl,\n lodMetaUrl,\n sogUrl,\n compressedPlyUrl,\n selected: splatUrl\n });\n\n // Transform waypoints\n const waypoints = (scene.waypoints || []).map((wp: WaypointData) => {\n // Handle FOV - convert from radians to degrees if necessary\n // (matching waypointNavigation.ts logic)\n let fov = wp.fov || 60;\n if (fov < 3.5) {\n // FOV is in radians, convert to degrees\n fov = fov * (180 / Math.PI);\n }\n\n // Extract info from interactions (type: 'info') if not directly on waypoint\n // In SceneTypes.ts, waypoint info is stored in interactions array with type 'info'\n let info = (wp as any).info || (wp as any).description || '';\n if (!info && wp.interactions) {\n const infoInteraction = wp.interactions.find((i: any) => i.type === 'info');\n if (infoInteraction && infoInteraction.data && (infoInteraction.data as any).text) {\n info = (infoInteraction.data as any).text;\n }\n }\n\n return {\n position: wp.position || { x: wp.x || 0, y: wp.y || 0, z: wp.z || 0 },\n rotation: wp.rotation || { x: 0, y: 0, z: 0 },\n fov,\n duration: wp.duration || 2000,\n name: wp.name || (wp as any).title || '',\n info,\n interactions: wp.interactions || [],\n triggerDistance: (wp as any).triggerDistance ?? 1.0\n };\n });\n\n // Build ExportProps\n const exportProps: ExportProps = {\n // Scene Metadata\n name: scene.name || 'StorySplat Scene',\n sceneId: scene.sceneId,\n userId: scene.userId,\n userName: scene.userName || 'Unknown', // Fallback for anonymous/deleted users\n thumbnailUrl: scene.thumbnailUrl,\n\n // Splat Configuration\n splatUrl,\n sogUrl,\n lodMetaUrl, // LOD streaming format URL (highest priority)\n // Build fallback URLs from all available formats (excluding the primary)\n fallbackUrls: [\n ...(scene.fallbackUrls || []),\n // Add other available formats as fallbacks\n sogUrl !== splatUrl ? sogUrl : null,\n compressedPlyUrl !== splatUrl ? compressedPlyUrl : null,\n originalUrl !== splatUrl ? originalUrl : null\n ].filter((url): url is string => url != null && url !== ''),\n // Handle both splatScale (number) and scale (object) formats\n scale: scene.scale || (\n scene.splatScale != null\n ? { x: scene.splatScale, y: scene.splatScale, z: scene.splatScale }\n : { x: 1, y: 1, z: 1 }\n ),\n // Handle both array and object position formats\n splatPosition: scene.splatPosition || scene.position || scene.cameraPosition || [0, 0, 0],\n splatRotation: scene.splatRotation || scene.rotation || scene.cameraRotation || [0, 0, 0],\n invertXScale: scene.invertXScale,\n invertYScale: scene.invertYScale,\n\n // Scene Data\n waypoints,\n hotspots: scene.hotspots || [],\n portals: scene.portals || [],\n skybox: scene.skybox,\n customMeshes: scene.customMeshes || [],\n htmlMeshes: scene.htmlMeshes || [],\n lights: scene.lights || [],\n particles: scene.particles || [],\n collisionMeshesData: scene.collisionMeshesData || [],\n playerHeight: scene.playerHeight,\n\n // UI Configuration\n uiColor: scene.uiColor || '#ffffff',\n uiOptions: {\n showStartExperience: scene.uiOptions?.showStartExperience ?? true,\n showWatermark: scene.uiOptions?.showWatermark ?? true,\n hideFullscreenButton: scene.uiOptions?.hideFullscreenButton ?? false,\n hideInfoButton: scene.uiOptions?.hideInfoButton ?? false,\n hideMuteButton: scene.uiOptions?.hideMuteButton ?? false,\n hideHelpButton: scene.uiOptions?.hideHelpButton ?? false,\n hideWatermark: scene.uiOptions?.hideWatermark ?? false,\n watermarkText: scene.uiOptions?.watermarkText,\n watermarkLink: scene.uiOptions?.watermarkLink,\n buttonPosition: scene.uiOptions?.buttonPosition || 'inline',\n buttonLabels: scene.uiOptions?.buttonLabels,\n // Whitelabeling option for custom preloader logo\n customPreloaderLogoUrl: scene.uiOptions?.customPreloaderLogoUrl\n },\n\n // Camera Configuration\n defaultCameraMode: scene.defaultCameraMode || 'orbit',\n allowedCameraModes: scene.allowedCameraModes || ['orbit', 'first-person', 'drone'],\n cameraMovementSpeed: scene.cameraMovementSpeed,\n cameraRotationSensitivity: scene.cameraRotationSensitivity,\n invertCameraRotation: scene.invertCameraRotation,\n\n // Navigation Configuration\n includeScrollControls: scene.includeScrollControls ?? true,\n scrollButtonMode: scene.scrollButtonMode || 'continuous',\n scrollAmount: scene.scrollAmount || 100,\n scrollSpeed: scene.scrollSpeed,\n transitionSpeed: scene.transitionSpeed,\n autoPlayEnabled: scene.autoPlayEnabled ?? false,\n // Playback settings\n autoplaySpeed: scene.autoplaySpeed,\n loopMode: scene.loopMode,\n\n // XR Configuration\n includeXR: scene.includeXR,\n xrMode: scene.xrMode,\n\n // Custom Script\n customScript: scene.customScript,\n\n // Template type (default to standard)\n templateType: 'standard',\n\n // Splat Swap System\n additionalSplats: scene.additionalSplats || [],\n keepMeshesInMemory: scene.keepMeshesInMemory ?? false\n };\n\n return exportProps;\n}\n\n/**\n * Viewer configuration for dynamic viewer runtime\n */\nexport interface ViewerConfig {\n splatUrl: string;\n sogUrl?: string;\n /** LOD streaming format URL (lod-meta.json) - highest priority for loading */\n lodMetaUrl?: string;\n fallbackUrls?: string[];\n scale?: { x: number; y: number; z: number };\n position: number[];\n rotation: number[];\n invertXScale?: boolean;\n invertYScale?: boolean;\n waypoints?: any[];\n hotspots?: any[];\n portals?: any[];\n /** Nested skybox config (newer format) */\n skybox?: { url: string; rotation?: number; intensity?: number; enableIBL?: boolean };\n /** Flat skybox URL (older format, for backwards compatibility) */\n skyboxUrl?: string;\n /** Flat skybox rotation in radians (older format, for backwards compatibility) */\n skyboxRotation?: number;\n customMeshes?: any[];\n htmlMeshes?: any[];\n lights?: any[];\n particles?: any[];\n collisionMeshesData?: any[];\n cameraMode?: string;\n defaultCameraMode?: string;\n allowedCameraModes?: string[];\n autoPlay?: boolean;\n uiColor?: string;\n uiOptions?: any;\n // Camera settings (matching HTML export CONFIG)\n fov?: number;\n nearClip?: number;\n farClip?: number;\n playerHeight?: number;\n cameraMovementSpeed?: number;\n cameraRotationSensitivity?: number;\n invertCameraRotation?: boolean;\n // Navigation settings\n scrollSpeed?: number;\n scrollAmount?: number;\n scrollButtonMode?: 'continuous' | 'incremental' | 'percentage' | 'waypoint';\n transitionSpeed?: number;\n // Playback settings\n /** Speed of autoplay in progress per second (default: calculated from waypoint durations) */\n autoplaySpeed?: number;\n /** Loop behavior for playback: 'loop' restarts at beginning, 'pingpong' reverses direction, 'none' stops at end */\n loopMode?: 'loop' | 'pingpong' | 'none';\n // Splat Swap System\n additionalSplats?: SplatSwapPoint[];\n keepMeshesInMemory?: boolean;\n // XR Configuration\n includeXR?: boolean;\n xrMode?: 'ar' | 'vr' | 'both';\n // Custom Script\n customScript?: string;\n // Background color\n backgroundColor?: string;\n}\n\n/**\n * Transform ExportProps to scene config for dynamic viewer\n * This is a simpler format for runtime use\n */\nexport function exportPropsToViewerConfig(props: ExportProps): ViewerConfig {\n // Get FOV from first waypoint if available, otherwise default to 60\n const fov = props.waypoints?.[0]?.fov || 60;\n\n return {\n splatUrl: props.splatUrl,\n sogUrl: props.sogUrl,\n lodMetaUrl: props.lodMetaUrl, // LOD streaming format (highest priority)\n fallbackUrls: props.fallbackUrls,\n scale: props.scale,\n position: props.splatPosition || [0, 0, 0],\n rotation: props.splatRotation || [0, 0, 0],\n invertXScale: (props as any).invertXScale,\n invertYScale: props.invertYScale,\n waypoints: props.waypoints,\n hotspots: props.hotspots,\n portals: props.portals,\n skybox: props.skybox,\n customMeshes: props.customMeshes,\n htmlMeshes: props.htmlMeshes,\n lights: props.lights,\n particles: props.particles,\n collisionMeshesData: props.collisionMeshesData,\n cameraMode: props.defaultCameraMode,\n defaultCameraMode: props.defaultCameraMode,\n allowedCameraModes: props.allowedCameraModes,\n autoPlay: props.autoPlayEnabled,\n uiColor: props.uiColor,\n uiOptions: props.uiOptions,\n // Camera settings (matching HTML export CONFIG)\n fov: fov,\n nearClip: (props as any).minClipPlane || 0.1,\n farClip: (props as any).maxClipPlane || 1000,\n playerHeight: props.playerHeight || 1.6,\n cameraMovementSpeed: props.cameraMovementSpeed || 1,\n cameraRotationSensitivity: props.cameraRotationSensitivity || 0.2,\n invertCameraRotation: (props as any).invertCameraRotation,\n // Navigation settings\n scrollSpeed: props.scrollSpeed,\n scrollAmount: props.scrollAmount,\n scrollButtonMode: props.scrollButtonMode,\n transitionSpeed: (props as any).transitionSpeed,\n // Playback settings\n autoplaySpeed: (props as any).autoplaySpeed,\n loopMode: (props as any).loopMode,\n // Splat Swap System\n additionalSplats: props.additionalSplats,\n keepMeshesInMemory: props.keepMeshesInMemory ?? false,\n // XR Configuration\n includeXR: props.includeXR,\n xrMode: props.xrMode,\n // Custom Script\n customScript: props.customScript\n };\n}\n","/**\n * Dynamic Viewer UI Module\n *\n * Generates and injects UI elements and CSS for the dynamic viewer.\n * Uses MINIMAL theme for embedded viewers.\n */\n\nimport type { ViewerConfig, ButtonLabels } from '../types';\n\n/**\n * Default button labels for internationalization (i18n)\n */\nconst DEFAULT_BUTTON_LABELS: Required<ButtonLabels> = {\n tour: 'Tour',\n explore: 'Explore',\n hybrid: 'Hybrid',\n walk: 'Walk',\n orbit: 'Orbit',\n next: 'Next',\n previous: 'Prev',\n startExperience: 'Start Experience',\n returnToTour: 'Return to Tour',\n fullscreen: 'Fullscreen',\n mute: 'Mute',\n unmute: 'Unmute',\n helpTitle: 'Controls & Help',\n percentageFormat: '{n}%'\n};\n\n/**\n * Get a button label, falling back to default if not provided\n */\nfunction getButtonLabel(labels: ButtonLabels | undefined, key: keyof ButtonLabels): string {\n return labels?.[key] || DEFAULT_BUTTON_LABELS[key];\n}\n\n/**\n * Format a percentage label using the configured format\n */\nfunction formatPercentageLabel(labels: ButtonLabels | undefined, value: number): string {\n const format = labels?.percentageFormat || DEFAULT_BUTTON_LABELS.percentageFormat;\n return format.replace('{n}', String(value));\n}\n\nexport interface UIOptions {\n uiColor?: string;\n showScrollControls?: boolean;\n showModeToggle?: boolean;\n showFullscreenButton?: boolean;\n showHelpButton?: boolean;\n showPreloader?: boolean;\n allowedCameraModes?: string[];\n defaultCameraMode?: string;\n customPreloaderLogoUrl?: string;\n /** Custom button labels for internationalization (i18n) */\n buttonLabels?: ButtonLabels;\n /** Whether to hide the watermark (whitelabeling) */\n hideWatermark?: boolean;\n /** Custom watermark text (whitelabeling) */\n watermarkText?: string;\n /** Custom watermark link URL (whitelabeling) */\n watermarkLink?: string;\n /** Scene ID for default watermark link */\n sceneId?: string;\n}\n\nexport interface UIElements {\n preloader?: HTMLElement;\n scrollControls?: HTMLElement;\n modeToggle?: HTMLElement;\n fullscreenButton?: HTMLElement;\n helpButton?: HTMLElement;\n helpPanel?: HTMLElement;\n waypointInfo?: HTMLElement;\n progressBar?: HTMLElement;\n progressText?: HTMLElement;\n hotspotPopup?: HTMLElement;\n joystick?: HTMLElement;\n joystickThumb?: HTMLElement;\n lookZone?: HTMLElement;\n cameraModeToggle?: HTMLElement;\n returnWaypointButton?: HTMLElement;\n vrButton?: HTMLElement;\n arButton?: HTMLElement;\n watermark?: HTMLElement;\n}\n\n/**\n * Generate MINIMAL CSS styles for the viewer UI\n */\nexport function generateViewerStyles(uiColor: string = '#4CAF50'): string {\n return `\n /* Container - CRITICAL: must be position relative and contained */\n .storysplat-viewer-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-family: system-ui, -apple-system, sans-serif;\n box-sizing: border-box;\n }\n\n .storysplat-viewer-container * {\n box-sizing: border-box;\n }\n\n .storysplat-viewer-container canvas {\n position: absolute;\n top: 0;\n left: 0;\n width: 100% !important;\n height: 100% !important;\n display: block;\n touch-action: none;\n }\n\n /* Preloader - semi-transparent to see loading/reveal effect */\n .storysplat-preloader {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(30, 30, 30, 0.85);\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n z-index: 100000;\n transition: opacity 0.5s ease-out;\n }\n\n .storysplat-preloader.hidden {\n opacity: 0;\n pointer-events: none;\n }\n\n .storysplat-preloader-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 40px;\n gap: 20px;\n }\n\n .storysplat-preloader-media {\n display: flex;\n align-items: center;\n gap: 40px;\n }\n\n .storysplat-preloader-image {\n height: 200px;\n width: auto;\n object-fit: contain;\n }\n\n .storysplat-preloader-image-inverted {\n height: 200px;\n width: auto;\n object-fit: contain;\n filter: invert(1);\n margin-right: -100px;\n }\n\n .storysplat-preloader-lottie {\n height: 200px;\n width: 200px;\n }\n\n @media (max-width: 768px) {\n .storysplat-preloader-image,\n .storysplat-preloader-image-inverted {\n height: 100px;\n }\n .storysplat-preloader-image-inverted {\n margin-right: -50px;\n }\n .storysplat-preloader-lottie {\n height: 100px;\n width: 100px;\n }\n }\n\n .storysplat-preloader-progress {\n width: 200px;\n height: 4px;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 2px;\n margin-top: 20px;\n position: relative;\n }\n\n .storysplat-preloader-bar {\n width: 0%;\n height: 100%;\n background: ${uiColor};\n border-radius: 2px;\n transition: width 0.3s ease-out;\n }\n\n .storysplat-preloader-text {\n position: absolute;\n top: -25px;\n left: 50%;\n transform: translateX(-50%);\n color: white;\n font-size: 14px;\n white-space: nowrap;\n }\n\n /* Minimal Scroll Controls */\n .storysplat-scroll-controls {\n position: absolute;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n width: 150px;\n padding: 10px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n z-index: 1000;\n }\n\n .storysplat-scroll-content {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n }\n\n .storysplat-progress-text {\n font-size: 14px;\n color: white;\n font-variant-numeric: tabular-nums;\n min-width: 40px;\n text-align: center;\n }\n\n .storysplat-progress-container {\n width: 90%;\n max-width: 150px;\n height: 3px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 2px;\n overflow: hidden;\n }\n\n .storysplat-progress-bar {\n height: 100%;\n background: ${uiColor};\n transition: width 0.3s ease-out;\n width: 0%;\n }\n\n .storysplat-scroll-buttons {\n display: flex;\n justify-content: center;\n gap: 5px;\n z-index: 1000;\n }\n\n /* Minimal Button Style */\n .storysplat-btn {\n background: rgba(0, 0, 0, 0.3);\n border: none;\n color: white;\n padding: 4px 8px;\n font-size: 12px;\n border-radius: 3px;\n cursor: pointer;\n transition: background 0.2s;\n }\n\n .storysplat-btn:hover {\n background: rgba(0, 0, 0, 0.5);\n }\n\n .storysplat-btn-play {\n background: rgba(0, 0, 0, 0.3);\n border: none;\n color: white;\n padding: 4px 8px;\n border-radius: 3px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .storysplat-btn-play svg {\n width: 12px;\n height: 12px;\n fill: white;\n }\n\n /* Mode Toggle - Minimal */\n .storysplat-mode-container {\n margin-top: 5px;\n }\n\n .storysplat-mode-toggle {\n display: flex;\n gap: 5px;\n }\n\n .storysplat-mode-btn {\n background: rgba(0, 0, 0, 0.3);\n border: none;\n color: white;\n padding: 3px 6px;\n font-size: 11px;\n border-radius: 2px;\n cursor: pointer;\n transition: all 0.2s ease;\n }\n\n .storysplat-mode-btn.selected {\n background: ${uiColor} !important;\n }\n\n .storysplat-mode-btn:hover {\n background: rgba(0, 0, 0, 0.5);\n }\n\n /* Fullscreen Button - Minimal */\n .storysplat-fullscreen-btn {\n position: absolute;\n top: 10px;\n right: 10px;\n background: rgba(0, 0, 0, 0);\n border: none;\n padding: 5px;\n border-radius: 3px;\n cursor: pointer;\n z-index: 1000;\n }\n\n .storysplat-fullscreen-btn svg {\n width: 20px;\n height: 20px;\n fill: white;\n }\n\n /* Help Button - Minimal */\n .storysplat-help-btn {\n position: absolute;\n top: 10px;\n left: 10px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.3);\n color: white;\n border: none;\n cursor: pointer;\n font-size: 16px;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n transition: all 0.2s ease;\n }\n\n .storysplat-help-btn:hover {\n background: rgba(0, 0, 0, 0.5);\n }\n\n .storysplat-help-panel {\n position: absolute;\n top: 50px;\n left: 10px;\n background: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 15px;\n border-radius: 5px;\n max-width: 280px;\n z-index: 999;\n display: none;\n font-size: 12px;\n }\n\n .storysplat-help-panel.visible {\n display: block;\n }\n\n /* XR (VR/AR) Buttons - Minimal */\n .storysplat-xr-btn {\n position: absolute;\n top: 10px;\n right: 45px;\n background: rgba(0, 0, 0, 0.5);\n border: none;\n padding: 6px 10px;\n border-radius: 4px;\n cursor: pointer;\n z-index: 1000;\n color: white;\n font-size: 11px;\n font-weight: bold;\n text-transform: uppercase;\n transition: all 0.2s ease;\n display: none;\n }\n\n .storysplat-xr-btn.available {\n display: block;\n }\n\n .storysplat-xr-btn.active {\n background: ${uiColor};\n }\n\n .storysplat-xr-btn:hover {\n background: rgba(0, 0, 0, 0.7);\n }\n\n .storysplat-xr-btn.active:hover {\n background: ${uiColor};\n opacity: 0.9;\n }\n\n .storysplat-ar-btn {\n right: 85px;\n }\n\n .storysplat-help-panel h3 {\n margin: 0 0 10px 0;\n font-size: 14px;\n font-weight: 600;\n }\n\n .storysplat-help-panel p {\n margin: 5px 0;\n line-height: 1.4;\n }\n\n /* Waypoint Info - Top Banner Style (matching BabylonJS minimal export) */\n .storysplat-waypoint-info {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n background: rgba(0, 0, 0, 0.5);\n color: white;\n text-align: center;\n z-index: 1000;\n pointer-events: none;\n display: none;\n }\n\n .storysplat-waypoint-info.hasContent {\n display: block;\n padding: 50px 30px 30px 30px;\n }\n\n .storysplat-waypoint-info h2 {\n margin: 0 0 8px 0;\n font-size: 18px;\n font-weight: bold;\n }\n\n .storysplat-waypoint-info p {\n margin: 0;\n font-size: 14px;\n line-height: 1.5;\n opacity: 0.9;\n }\n\n /* Responsive waypoint info */\n @media (max-height: 600px) {\n .storysplat-waypoint-info.hasContent {\n padding: 20px 20px 15px 20px;\n font-size: 13px;\n }\n }\n\n @media (max-height: 500px) {\n .storysplat-waypoint-info.hasContent {\n padding: 10px 15px 10px 15px;\n font-size: 12px;\n }\n }\n\n /* Hotspot Popup - Matching BabylonJS HTML export styles */\n /* Uses position: absolute so popup stays within container when embedded */\n .storysplat-hotspot-popup {\n position: absolute;\n background-color: rgba(0, 0, 0, 0.8);\n color: white;\n padding: 20px;\n border-radius: 10px;\n z-index: 100001;\n box-shadow: 0 0 10px rgba(0,0,0,0.5);\n display: none;\n font-size: 14px;\n flex-direction: column;\n font-family: system-ui, -apple-system, sans-serif;\n /* Default centered positioning */\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n max-width: 80%;\n max-height: 80%;\n overflow: auto;\n }\n\n .storysplat-hotspot-popup.visible {\n display: flex;\n }\n\n /* Fullscreen mode for click activation with image/iframe content */\n .storysplat-hotspot-popup.fullscreen {\n top: 0 !important;\n left: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n transform: none !important;\n width: 100% !important;\n height: 100% !important;\n max-width: 100% !important;\n max-height: 100% !important;\n margin: 0 !important;\n border-radius: 0 !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n flex-direction: column !important;\n padding: 0 !important;\n }\n\n .storysplat-hotspot-popup h2,\n .storysplat-hotspot-popup-title {\n margin: 0 0 10px 0;\n font-size: 18px;\n font-weight: 600;\n padding-right: 30px;\n }\n\n .storysplat-hotspot-popup p {\n margin: 0 0 10px 0;\n line-height: 1.5;\n font-size: 14px;\n white-space: pre-wrap;\n }\n\n .storysplat-hotspot-popup img {\n max-width: 100%;\n height: auto;\n border-radius: 5px;\n margin-bottom: 10px;\n display: block;\n }\n\n .storysplat-hotspot-popup.fullscreen img {\n width: 80% !important;\n height: 80% !important;\n max-width: 80% !important;\n object-fit: contain !important;\n border: 2px solid rgba(255, 255, 255);\n margin: 30px 0;\n }\n\n .storysplat-hotspot-popup iframe {\n width: 100%;\n height: 400px;\n border: none;\n border-radius: 5px;\n margin-bottom: 10px;\n }\n\n .storysplat-hotspot-popup.fullscreen iframe {\n width: 80% !important;\n height: 80% !important;\n margin: 30px 0;\n }\n\n .storysplat-hotspot-popup video {\n max-width: 100%;\n height: auto;\n border-radius: 5px;\n margin-bottom: 10px;\n display: block;\n }\n\n .storysplat-hotspot-popup.fullscreen video {\n width: 80% !important;\n height: 80% !important;\n max-width: 80% !important;\n object-fit: contain !important;\n border: 2px solid rgba(255, 255, 255);\n margin: 30px 0;\n }\n\n /* Close button - matching BabylonJS export (green button at bottom) */\n .storysplat-hotspot-popup-close {\n width: auto;\n padding: 10px 20px;\n background-color: #4CAF50;\n border: none;\n color: white;\n cursor: pointer;\n border-radius: 5px;\n margin: 10px 0 0 0;\n font-size: 14px;\n font-weight: 500;\n transition: background-color 0.2s;\n }\n\n .storysplat-hotspot-popup-close:hover {\n background-color: #45a049;\n }\n\n .storysplat-hotspot-popup-link {\n display: block;\n padding: 8px 16px;\n background: #007bff;\n color: white;\n text-decoration: none;\n border-radius: 4px;\n text-align: center;\n font-weight: 500;\n margin: 0 0 10px 0;\n cursor: pointer;\n transition: background-color 0.2s;\n }\n\n .storysplat-hotspot-popup-link:hover {\n background: #0056b3;\n }\n\n /* Mobile Responsive */\n @media (max-width: 768px) {\n .storysplat-scroll-controls {\n margin-bottom: 10px;\n }\n }\n\n @media (max-width: 540px) {\n .storysplat-scroll-controls {\n margin-bottom: 45px;\n }\n }\n\n /* Virtual Joystick Overlay - visual indicator only, touches pass through to canvas */\n .storysplat-joystick-container {\n position: absolute;\n bottom: 80px;\n left: 40px;\n width: 120px;\n height: 120px;\n z-index: 2000;\n pointer-events: none;\n touch-action: none;\n display: none;\n }\n\n .storysplat-joystick-container.visible {\n display: block;\n }\n\n .storysplat-joystick-base {\n position: absolute;\n top: 0;\n left: 0;\n width: 120px;\n height: 120px;\n background: rgba(255, 255, 255, 0.2);\n border: 3px solid rgba(255, 255, 255, 0.4);\n border-radius: 50%;\n pointer-events: none;\n }\n\n .storysplat-joystick-thumb {\n position: absolute;\n top: 50%;\n left: 50%;\n width: 50px;\n height: 50px;\n margin-left: -25px;\n margin-top: -25px;\n background: rgba(255, 255, 255, 0.6);\n border: 3px solid rgba(255, 255, 255, 0.8);\n border-radius: 50%;\n pointer-events: none;\n transition: transform 0.05s ease-out;\n }\n\n .storysplat-joystick-thumb.active {\n background: rgba(255, 255, 255, 0.8);\n border-color: white;\n }\n\n /* Look Zone Indicator (right side) */\n .storysplat-look-zone {\n position: absolute;\n bottom: 80px;\n right: 40px;\n width: 100px;\n height: 100px;\n z-index: 1000;\n pointer-events: none;\n display: none;\n }\n\n .storysplat-look-zone.visible {\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .storysplat-look-zone-icon {\n width: 60px;\n height: 60px;\n background: rgba(255, 255, 255, 0.15);\n border: 2px solid rgba(255, 255, 255, 0.25);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .storysplat-look-zone-icon svg {\n width: 30px;\n height: 30px;\n fill: rgba(255, 255, 255, 0.5);\n }\n\n /* Camera Mode Toggle (Orbit/Fly) - Mobile only in explore mode */\n .storysplat-camera-mode-toggle {\n position: absolute;\n bottom: 210px;\n left: 40px;\n width: 44px;\n height: 44px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.5);\n border: 2px solid rgba(255, 255, 255, 0.3);\n cursor: pointer;\n z-index: 2001;\n display: none;\n align-items: center;\n justify-content: center;\n touch-action: manipulation;\n -webkit-tap-highlight-color: transparent;\n }\n\n .storysplat-camera-mode-toggle.visible {\n display: flex;\n }\n\n .storysplat-camera-mode-toggle:active {\n background: rgba(0, 0, 0, 0.7);\n transform: scale(0.95);\n }\n\n .storysplat-camera-mode-toggle svg {\n width: 24px;\n height: 24px;\n fill: white;\n }\n\n .storysplat-camera-mode-toggle .orbit-icon,\n .storysplat-camera-mode-toggle .fly-icon {\n display: none;\n }\n\n .storysplat-camera-mode-toggle.orbit .orbit-icon {\n display: block;\n }\n\n .storysplat-camera-mode-toggle.fly .fly-icon {\n display: block;\n }\n\n /* Return to Waypoint Button - Explore mode only */\n .storysplat-return-waypoint-btn {\n position: absolute;\n bottom: 20px;\n left: 20px;\n padding: 10px 16px;\n background: rgba(0, 0, 0, 0.6);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 8px;\n color: white;\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n z-index: 2001;\n display: none;\n align-items: center;\n gap: 8px;\n transition: all 0.2s ease;\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n }\n\n .storysplat-return-waypoint-btn.visible {\n display: flex;\n }\n\n .storysplat-return-waypoint-btn:hover {\n background: rgba(0, 0, 0, 0.8);\n border-color: rgba(255, 255, 255, 0.5);\n }\n\n .storysplat-return-waypoint-btn:active {\n transform: scale(0.98);\n }\n\n .storysplat-return-waypoint-btn svg {\n width: 16px;\n height: 16px;\n fill: white;\n }\n\n @media (max-width: 768px) {\n .storysplat-return-waypoint-btn {\n bottom: auto;\n top: 60px;\n left: 10px;\n padding: 8px 12px;\n font-size: 12px;\n }\n }\n\n /* Lazy Load Container */\n .storysplat-lazy-load-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background: #1a1a1a;\n z-index: 10001;\n }\n\n .storysplat-lazy-load-thumbnail {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 0.7;\n }\n\n .storysplat-lazy-load-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: linear-gradient(\n to bottom,\n rgba(0, 0, 0, 0.3) 0%,\n rgba(0, 0, 0, 0.5) 50%,\n rgba(0, 0, 0, 0.7) 100%\n );\n }\n\n .storysplat-lazy-load-content {\n position: relative;\n z-index: 1;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n }\n\n .storysplat-lazy-load-start-btn {\n padding: 16px 32px;\n background: ${uiColor};\n border: none;\n border-radius: 50px;\n color: white;\n font-size: 18px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n transition: all 0.3s ease;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n }\n\n .storysplat-lazy-load-start-btn:hover {\n transform: scale(1.05);\n box-shadow: 0 6px 30px rgba(0, 0, 0, 0.4);\n }\n\n .storysplat-lazy-load-start-btn:active {\n transform: scale(0.98);\n }\n\n .storysplat-lazy-load-start-btn svg {\n width: 24px;\n height: 24px;\n fill: white;\n }\n\n @media (max-width: 768px) {\n .storysplat-lazy-load-start-btn {\n padding: 12px 24px;\n font-size: 16px;\n }\n }\n\n /* Watermark - matches BabylonJS HTML export */\n .storysplat-watermark {\n position: fixed;\n bottom: 10px;\n right: 10px;\n background-color: rgba(0, 0, 0, 0.5);\n color: white;\n padding: 5px 10px;\n border-radius: 5px;\n font-size: 12px;\n z-index: 1000;\n pointer-events: auto; /* Allow pointer events for the entire watermark so links work */\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n cursor: default;\n }\n\n .storysplat-watermark a {\n color: ${uiColor};\n text-decoration: none;\n cursor: pointer;\n }\n\n .storysplat-watermark a:hover {\n text-decoration: underline;\n }\n `;\n}\n\n/**\n * Inject CSS styles into the document\n */\nexport function injectStyles(uiColor: string = '#4CAF50'): HTMLStyleElement {\n const existingStyle = document.getElementById('storysplat-viewer-styles');\n if (existingStyle) {\n existingStyle.remove();\n }\n\n const style = document.createElement('style');\n style.id = 'storysplat-viewer-styles';\n style.textContent = generateViewerStyles(uiColor);\n document.head.appendChild(style);\n return style;\n}\n\n// StorySplat default assets\nconst STORYSPLAT_LOGO_URL = 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fimages%2FStorySplat.webp?alt=media&token=953e8ab3-1865-4ac1-a98d-b548b7066bda';\nconst STORYSPLAT_LOTTIE_URL = 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Flotties%2FstorySplatLottie.json?alt=media&token=d7edc19d-9cb8-4c6e-a94c-cba1d2b65d5e';\n\n/**\n * Inject Lottie player script if not already loaded\n */\nfunction ensureLottiePlayer(): Promise<void> {\n return new Promise((resolve) => {\n // Check if already loaded\n if (customElements.get('lottie-player')) {\n resolve();\n return;\n }\n\n // Check if script is already being loaded\n const existingScript = document.querySelector('script[src*=\"lottie-player\"]');\n if (existingScript) {\n existingScript.addEventListener('load', () => resolve());\n return;\n }\n\n // Load the script\n const script = document.createElement('script');\n script.src = 'https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js';\n script.onload = () => resolve();\n document.head.appendChild(script);\n });\n}\n\n/**\n * Create preloader element\n */\nexport function createPreloader(container: HTMLElement, customLogoUrl?: string): HTMLElement {\n const preloader = document.createElement('div');\n preloader.className = 'storysplat-preloader';\n\n const useCustomLogo = !!customLogoUrl;\n\n preloader.innerHTML = `\n <div class=\"storysplat-preloader-content\">\n <div class=\"storysplat-preloader-media\">\n ${useCustomLogo\n ? `<img class=\"storysplat-preloader-image\" src=\"${customLogoUrl}\" alt=\"Custom Logo\" />`\n : `<img class=\"storysplat-preloader-image-inverted\" src=\"${STORYSPLAT_LOGO_URL}\" alt=\"StorySplat Logo\" />`\n }\n ${!useCustomLogo\n ? `<lottie-player class=\"storysplat-preloader-lottie\"\n src=\"${STORYSPLAT_LOTTIE_URL}\"\n background=\"transparent\"\n speed=\"1\"\n loop\n autoplay>\n </lottie-player>`\n : ''\n }\n </div>\n <div class=\"storysplat-preloader-progress\">\n <div class=\"storysplat-preloader-text\">Loading... 0%</div>\n <div class=\"storysplat-preloader-bar\"></div>\n </div>\n </div>\n `;\n container.appendChild(preloader);\n\n // Load Lottie player script if using default logo\n if (!useCustomLogo) {\n ensureLottiePlayer();\n }\n\n return preloader;\n}\n\n/**\n * Update preloader progress\n */\nexport function updatePreloaderProgress(preloader: HTMLElement, progress: number, text?: string): void {\n const bar = preloader.querySelector('.storysplat-preloader-bar') as HTMLElement;\n const textEl = preloader.querySelector('.storysplat-preloader-text') as HTMLElement;\n const percent = Math.max(0, Math.min(100, progress * 100));\n if (bar) bar.style.width = `${percent}%`;\n if (textEl) textEl.textContent = text || `Loading... ${Math.round(percent)}%`;\n}\n\n/**\n * Hide preloader\n */\nexport function hidePreloader(preloader: HTMLElement): void {\n preloader.classList.add('hidden');\n setTimeout(() => preloader.remove(), 500);\n}\n\n/**\n * Create UI elements for the viewer\n */\nexport function createUIElements(\n container: HTMLElement,\n config: ViewerConfig,\n options: UIOptions = {}\n): UIElements {\n const {\n uiColor = '#4CAF50',\n showScrollControls = true,\n showModeToggle = true,\n showFullscreenButton = true,\n showHelpButton = false, // Minimal: hide by default\n showPreloader = true,\n allowedCameraModes = ['tour', 'explore'],\n defaultCameraMode = 'tour',\n customPreloaderLogoUrl,\n buttonLabels,\n hideWatermark = false,\n watermarkText,\n watermarkLink,\n sceneId\n } = options;\n\n // Get localized labels\n const labels = {\n tour: getButtonLabel(buttonLabels, 'tour'),\n explore: getButtonLabel(buttonLabels, 'explore'),\n walk: getButtonLabel(buttonLabels, 'walk'),\n previous: getButtonLabel(buttonLabels, 'previous'),\n next: getButtonLabel(buttonLabels, 'next'),\n helpTitle: getButtonLabel(buttonLabels, 'helpTitle'),\n returnToTour: getButtonLabel(buttonLabels, 'returnToTour'),\n };\n\n const elements: UIElements = {};\n\n // Clean up any existing UI elements from previous renders\n container.querySelectorAll('.storysplat-preloader, .storysplat-scroll-controls, .storysplat-waypoint-info, .storysplat-fullscreen-btn, .storysplat-help-btn, .storysplat-help-panel, .storysplat-watermark').forEach(el => el.remove());\n\n // Inject styles\n injectStyles(uiColor);\n\n // Add container class\n container.classList.add('storysplat-viewer-container');\n\n // Create Preloader\n if (showPreloader) {\n elements.preloader = createPreloader(container, customPreloaderLogoUrl);\n }\n\n // Create Waypoint Info Panel (top banner style)\n const waypointInfo = document.createElement('div');\n waypointInfo.className = 'storysplat-waypoint-info';\n waypointInfo.innerHTML = `\n <h2 class=\"storysplat-waypoint-title\"></h2>\n <p class=\"storysplat-waypoint-description\"></p>\n `;\n container.appendChild(waypointInfo);\n elements.waypointInfo = waypointInfo;\n\n // Create Scroll Controls\n if (showScrollControls && config.waypoints && config.waypoints.length > 0) {\n const scrollControls = document.createElement('div');\n scrollControls.className = 'storysplat-scroll-controls';\n scrollControls.innerHTML = `\n <div class=\"storysplat-scroll-content\">\n <div class=\"storysplat-progress-text\">0%</div>\n <div class=\"storysplat-progress-container\">\n <div class=\"storysplat-progress-bar\"></div>\n </div>\n <div class=\"storysplat-scroll-buttons\">\n <button class=\"storysplat-btn storysplat-btn-prev\">${labels.previous}</button>\n <button class=\"storysplat-btn storysplat-btn-play\">\n <svg viewBox=\"0 0 24 24\"><path d=\"M8 5v14l11-7z\"/></svg>\n </button>\n <button class=\"storysplat-btn storysplat-btn-next\">${labels.next}</button>\n </div>\n ${showModeToggle && allowedCameraModes.length > 1 ? `\n <div class=\"storysplat-mode-container\">\n <div class=\"storysplat-mode-toggle\">\n ${allowedCameraModes.includes('tour') ? `<button class=\"storysplat-mode-btn ${defaultCameraMode === 'tour' ? 'selected' : ''}\" data-mode=\"tour\">${labels.tour}</button>` : ''}\n ${allowedCameraModes.includes('explore') ? `<button class=\"storysplat-mode-btn ${defaultCameraMode === 'explore' ? 'selected' : ''}\" data-mode=\"explore\">${labels.explore}</button>` : ''}\n ${allowedCameraModes.includes('walk') ? `<button class=\"storysplat-mode-btn ${defaultCameraMode === 'walk' ? 'selected' : ''}\" data-mode=\"walk\">${labels.walk}</button>` : ''}\n </div>\n </div>\n ` : ''}\n </div>\n `;\n container.appendChild(scrollControls);\n elements.scrollControls = scrollControls;\n elements.progressBar = scrollControls.querySelector('.storysplat-progress-bar') as HTMLElement;\n elements.progressText = scrollControls.querySelector('.storysplat-progress-text') as HTMLElement;\n }\n\n // Create Fullscreen Button\n if (showFullscreenButton) {\n const fullscreenBtn = document.createElement('button');\n fullscreenBtn.className = 'storysplat-fullscreen-btn';\n fullscreenBtn.setAttribute('aria-label', 'Toggle Fullscreen');\n fullscreenBtn.innerHTML = `\n <svg class=\"storysplat-expand-icon\" viewBox=\"0 0 24 24\">\n <path d=\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\"/>\n </svg>\n <svg class=\"storysplat-compress-icon\" viewBox=\"0 0 24 24\" style=\"display: none;\">\n <path d=\"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z\"/>\n </svg>\n `;\n container.appendChild(fullscreenBtn);\n elements.fullscreenButton = fullscreenBtn;\n }\n\n // Create VR Button (hidden by default, shown when VR is available)\n const vrBtn = document.createElement('button');\n vrBtn.className = 'storysplat-xr-btn storysplat-vr-btn';\n vrBtn.setAttribute('aria-label', 'Enter VR');\n vrBtn.textContent = 'VR';\n container.appendChild(vrBtn);\n elements.vrButton = vrBtn;\n\n // Create AR Button (hidden by default, shown when AR is available)\n const arBtn = document.createElement('button');\n arBtn.className = 'storysplat-xr-btn storysplat-ar-btn';\n arBtn.setAttribute('aria-label', 'Enter AR');\n arBtn.textContent = 'AR';\n container.appendChild(arBtn);\n elements.arButton = arBtn;\n\n // Create Help Button and Panel (optional in minimal)\n if (showHelpButton) {\n const helpBtn = document.createElement('button');\n helpBtn.className = 'storysplat-help-btn';\n helpBtn.setAttribute('title', 'Toggle Help');\n helpBtn.textContent = '?';\n container.appendChild(helpBtn);\n elements.helpButton = helpBtn;\n\n const helpPanel = document.createElement('div');\n helpPanel.className = 'storysplat-help-panel';\n helpPanel.innerHTML = `\n <h3>${labels.helpTitle}</h3>\n <p><strong>Camera Modes:</strong></p>\n <p>• ${labels.tour} - Follow predefined path</p>\n <p>• ${labels.explore} - Free movement</p>\n <p>• ${labels.walk} - First-person walking</p>\n <br/>\n <p><strong>${labels.tour} Mode:</strong></p>\n <p>• Scroll - Move along path</p>\n <p>• Drag - Look around</p>\n <br/>\n <p><strong>${labels.explore} Mode:</strong></p>\n <p>• LMB Drag - Orbit camera</p>\n <p>• RMB Drag - Fly/look</p>\n <p>• WASD/QE - Move camera</p>\n <p>• Shift - Move fast</p>\n <p>• Scroll/Pinch - Zoom</p>\n <p>• Double-click - Focus</p>\n <br/>\n <p><strong>${labels.walk} Mode:</strong></p>\n <p>• Click to lock mouse</p>\n <p>• WASD/Arrows - Move</p>\n <p>• Mouse - Look around</p>\n <p>• Shift - Sprint</p>\n <p>• Space - Jump</p>\n `;\n container.appendChild(helpPanel);\n elements.helpPanel = helpPanel;\n }\n\n // Create Hotspot Popup (always created, shown when needed)\n // Note: No overlay - matching BabylonJS export behavior\n const hotspotPopup = document.createElement('div');\n hotspotPopup.className = 'storysplat-hotspot-popup';\n hotspotPopup.id = 'hotspotContent'; // Match BabylonJS export ID\n hotspotPopup.innerHTML = `\n <h2 class=\"storysplat-hotspot-popup-title\"></h2>\n <div class=\"storysplat-hotspot-popup-content\"></div>\n <button class=\"storysplat-hotspot-popup-close\">Close</button>\n `;\n container.appendChild(hotspotPopup);\n elements.hotspotPopup = hotspotPopup;\n\n // Close popup on close button click\n const closeBtn = hotspotPopup.querySelector('.storysplat-hotspot-popup-close');\n closeBtn?.addEventListener('click', () => {\n hotspotPopup.classList.remove('visible', 'fullscreen');\n });\n\n // Create Virtual Joystick (for mobile explore mode)\n const joystick = document.createElement('div');\n joystick.className = 'storysplat-joystick-container';\n joystick.innerHTML = `\n <div class=\"storysplat-joystick-base\"></div>\n <div class=\"storysplat-joystick-thumb\"></div>\n `;\n container.appendChild(joystick);\n elements.joystick = joystick;\n elements.joystickThumb = joystick.querySelector('.storysplat-joystick-thumb') as HTMLElement;\n\n // Create Look Zone indicator (right side)\n const lookZone = document.createElement('div');\n lookZone.className = 'storysplat-look-zone';\n lookZone.innerHTML = `\n <div class=\"storysplat-look-zone-icon\">\n <svg viewBox=\"0 0 24 24\">\n <path d=\"M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z\"/>\n </svg>\n </div>\n `;\n container.appendChild(lookZone);\n elements.lookZone = lookZone;\n\n // Create Camera Mode Toggle (Orbit/Fly) for mobile explore mode\n const cameraModeToggle = document.createElement('button');\n cameraModeToggle.className = 'storysplat-camera-mode-toggle fly';\n cameraModeToggle.setAttribute('aria-label', 'Toggle camera mode');\n cameraModeToggle.innerHTML = `\n <svg class=\"orbit-icon\" viewBox=\"0 0 24 24\">\n <path d=\"M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z\"/>\n </svg>\n <svg class=\"fly-icon\" viewBox=\"0 0 24 24\">\n <path d=\"M12 2L4.5 20.29l.71.71L12 18l6.79 3 .71-.71z\"/>\n </svg>\n `;\n container.appendChild(cameraModeToggle);\n elements.cameraModeToggle = cameraModeToggle;\n\n // Create Return to Waypoint button (shown in explore mode)\n const returnWaypointButton = document.createElement('button');\n returnWaypointButton.className = 'storysplat-return-waypoint-btn';\n returnWaypointButton.setAttribute('aria-label', labels.returnToTour);\n returnWaypointButton.innerHTML = `\n <svg viewBox=\"0 0 24 24\">\n <path d=\"M9 11H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2zm2-7h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V9h14v11z\"/>\n </svg>\n ${labels.returnToTour}\n `;\n container.appendChild(returnWaypointButton);\n elements.returnWaypointButton = returnWaypointButton;\n\n // Create Watermark (unless hidden for whitelabeling)\n if (!hideWatermark) {\n const watermark = document.createElement('div');\n watermark.className = 'storysplat-watermark';\n\n // Build watermark content - matches BabylonJS HTML export behavior\n const defaultWatermarkLink = sceneId\n ? `https://storysplat.com?ref=${sceneId}`\n : 'https://storysplat.com';\n const finalLink = watermarkLink || defaultWatermarkLink;\n\n if (watermarkText) {\n // Custom text provided (whitelabeling)\n watermark.innerHTML = `<a href=\"${finalLink}\" target=\"_blank\">${watermarkText}</a>`;\n } else {\n // Default StorySplat watermark\n watermark.innerHTML = `Created with <a href=\"${finalLink}\" target=\"_blank\">StorySplat</a>`;\n }\n\n container.appendChild(watermark);\n elements.watermark = watermark;\n }\n\n return elements;\n}\n\n/**\n * Connect UI elements to viewer controls\n */\nexport function connectUIToViewer(\n elements: UIElements,\n viewer: {\n nextWaypoint: () => void;\n prevWaypoint: () => void;\n play: () => void;\n pause: () => void;\n isPlaying: () => boolean;\n getCurrentWaypointIndex: () => number;\n getWaypointCount: () => number;\n getWaypoints?: () => any[];\n setCameraMode?: (mode: string) => void;\n on: (event: string, callback: (...args: any[]) => void) => void;\n },\n defaultCameraMode: string = 'tour',\n buttonLabels?: ButtonLabels\n): void {\n // Prev/Next buttons\n const prevBtn = elements.scrollControls?.querySelector('.storysplat-btn-prev');\n const nextBtn = elements.scrollControls?.querySelector('.storysplat-btn-next');\n const playBtn = elements.scrollControls?.querySelector('.storysplat-btn-play');\n\n if (prevBtn) {\n prevBtn.addEventListener('click', () => viewer.prevWaypoint());\n }\n\n if (nextBtn) {\n nextBtn.addEventListener('click', () => viewer.nextWaypoint());\n }\n\n if (playBtn) {\n // Update button icon based on state\n const updatePlayButtonIcon = (isPlaying: boolean) => {\n if (isPlaying) {\n playBtn.innerHTML = '<svg viewBox=\"0 0 24 24\"><path d=\"M6 4h4v16H6zm8 0h4v16h-4z\"/></svg>'; // Pause icon\n } else {\n playBtn.innerHTML = '<svg viewBox=\"0 0 24 24\"><path d=\"M8 5v14l11-7z\"/></svg>'; // Play icon\n }\n };\n\n playBtn.addEventListener('click', () => {\n if (viewer.isPlaying()) {\n viewer.pause();\n } else {\n viewer.play();\n }\n // Icon will be updated by event listeners below\n });\n\n // Listen for playback events to update button state (handles autoplay and programmatic play/pause)\n viewer.on('playbackStart', () => updatePlayButtonIcon(true));\n viewer.on('playbackStop', () => updatePlayButtonIcon(false));\n\n // Set initial state based on current playing status\n updatePlayButtonIcon(viewer.isPlaying());\n }\n\n // Help button\n if (elements.helpButton && elements.helpPanel) {\n elements.helpButton.addEventListener('click', () => {\n elements.helpPanel!.classList.toggle('visible');\n });\n }\n\n // Fullscreen button\n if (elements.fullscreenButton) {\n // Detect iOS - iOS does not support the Fullscreen API\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||\n (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // iPad Pro detection\n\n // Hide fullscreen button on iOS since the API doesn't work there\n if (isIOS) {\n elements.fullscreenButton.style.display = 'none';\n console.log('[StorySplat Viewer] Fullscreen button hidden on iOS (API not supported)');\n } else {\n const container = elements.fullscreenButton.parentElement;\n elements.fullscreenButton.addEventListener('click', () => {\n // Use webkit-prefixed API for Safari compatibility\n const doc = document as any;\n const fullscreenElement = doc.fullscreenElement || doc.webkitFullscreenElement;\n\n if (!fullscreenElement) {\n // Enter fullscreen - try standard API first, then webkit\n if (container?.requestFullscreen) {\n container.requestFullscreen();\n } else if ((container as any)?.webkitRequestFullscreen) {\n (container as any).webkitRequestFullscreen();\n }\n const expandIcon = elements.fullscreenButton!.querySelector('.storysplat-expand-icon') as HTMLElement;\n const compressIcon = elements.fullscreenButton!.querySelector('.storysplat-compress-icon') as HTMLElement;\n if (expandIcon) expandIcon.style.display = 'none';\n if (compressIcon) compressIcon.style.display = 'block';\n } else {\n // Exit fullscreen - try standard API first, then webkit\n if (doc.exitFullscreen) {\n doc.exitFullscreen();\n } else if (doc.webkitExitFullscreen) {\n doc.webkitExitFullscreen();\n }\n const expandIcon = elements.fullscreenButton!.querySelector('.storysplat-expand-icon') as HTMLElement;\n const compressIcon = elements.fullscreenButton!.querySelector('.storysplat-compress-icon') as HTMLElement;\n if (expandIcon) expandIcon.style.display = 'block';\n if (compressIcon) compressIcon.style.display = 'none';\n }\n });\n\n // Listen for fullscreen change events (including webkit prefix)\n const handleFullscreenChange = () => {\n const doc = document as any;\n const isFullscreen = doc.fullscreenElement || doc.webkitFullscreenElement;\n const expandIcon = elements.fullscreenButton!.querySelector('.storysplat-expand-icon') as HTMLElement;\n const compressIcon = elements.fullscreenButton!.querySelector('.storysplat-compress-icon') as HTMLElement;\n if (expandIcon) expandIcon.style.display = isFullscreen ? 'none' : 'block';\n if (compressIcon) compressIcon.style.display = isFullscreen ? 'block' : 'none';\n };\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n document.addEventListener('webkitfullscreenchange', handleFullscreenChange);\n }\n }\n\n // Helper to show/hide tour navigation controls based on mode\n const setTourControlsVisible = (visible: boolean) => {\n const progressText = elements.scrollControls?.querySelector('.storysplat-progress-text') as HTMLElement;\n const progressContainer = elements.scrollControls?.querySelector('.storysplat-progress-container') as HTMLElement;\n const scrollButtons = elements.scrollControls?.querySelector('.storysplat-scroll-buttons') as HTMLElement;\n\n const display = visible ? '' : 'none';\n if (progressText) progressText.style.display = display;\n if (progressContainer) progressContainer.style.display = display;\n if (scrollButtons) scrollButtons.style.display = display;\n };\n\n // Set initial visibility based on default mode\n setTourControlsVisible(defaultCameraMode === 'tour');\n\n // Mode toggle buttons\n if (viewer.setCameraMode) {\n const modeButtons = elements.scrollControls?.querySelectorAll('.storysplat-mode-btn');\n modeButtons?.forEach(btn => {\n btn.addEventListener('click', () => {\n const mode = btn.getAttribute('data-mode');\n if (mode && viewer.setCameraMode) {\n viewer.setCameraMode(mode);\n // Update selected state\n modeButtons.forEach(b => b.classList.remove('selected'));\n btn.classList.add('selected');\n // Show/hide tour controls based on mode\n setTourControlsVisible(mode === 'tour');\n }\n });\n });\n\n // Set initial visibility based on default mode\n viewer.on('modeChange', ({ mode }: { mode: string }) => {\n setTourControlsVisible(mode === 'tour');\n // Update button selected state\n modeButtons?.forEach(btn => {\n const btnMode = btn.getAttribute('data-mode');\n btn.classList.toggle('selected', btnMode === mode);\n });\n });\n }\n\n // Update progress continuously (throttled to prevent visual artifacts)\n let lastProgressUpdate = 0;\n let lastDisplayedPercentage = -1;\n\n viewer.on('progressUpdate', ({ progress }: { progress: number }) => {\n // Clamp progress to 0-1 range\n const clampedProgress = Math.max(0, Math.min(1, progress));\n const percentage = clampedProgress * 100;\n const roundedPercentage = Math.round(percentage);\n\n // Throttle UI updates to ~30fps to prevent visual glitches\n const now = performance.now();\n if (now - lastProgressUpdate < 33) return;\n lastProgressUpdate = now;\n\n if (elements.progressBar) {\n elements.progressBar.style.width = `${percentage}%`;\n }\n\n // Only update text if the percentage actually changed (prevents flickering)\n if (elements.progressText && roundedPercentage !== lastDisplayedPercentage) {\n lastDisplayedPercentage = roundedPercentage;\n // Clear and set to ensure no duplicate content\n elements.progressText.innerHTML = '';\n elements.progressText.textContent = formatPercentageLabel(buttonLabels, roundedPercentage);\n }\n });\n\n // Update waypoint info panel when waypoint changes\n let lastDisplayedWaypointIndex = -1;\n viewer.on('waypointChange', ({ index, waypoint }: { index: number; waypoint?: any }) => {\n // Only update if waypoint actually changed\n if (index === lastDisplayedWaypointIndex) return;\n lastDisplayedWaypointIndex = index;\n\n if (!elements.waypointInfo) return;\n\n const titleEl = elements.waypointInfo.querySelector('.storysplat-waypoint-title') as HTMLElement;\n const descEl = elements.waypointInfo.querySelector('.storysplat-waypoint-description') as HTMLElement;\n\n if (!titleEl || !descEl) return;\n\n // Get waypoint data - either from event or from viewer\n const wp = waypoint || (viewer.getWaypoints?.()[index]);\n\n if (wp && (wp.name || wp.info)) {\n // Show waypoint name as title\n titleEl.textContent = wp.name || '';\n\n // Show waypoint info as description\n descEl.textContent = wp.info || '';\n\n // Show panel if there's content\n if (wp.name || wp.info) {\n elements.waypointInfo.classList.add('hasContent');\n } else {\n elements.waypointInfo.classList.remove('hasContent');\n }\n } else {\n // Hide panel if no content\n titleEl.textContent = '';\n descEl.textContent = '';\n elements.waypointInfo.classList.remove('hasContent');\n }\n });\n}\n\n/**\n * Show hotspot popup with content - matching BabylonJS HTML export behavior\n */\nexport function showHotspotPopup(container: HTMLElement, hotspot: any): void {\n const popup = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n\n if (!popup) return;\n\n const titleEl = popup.querySelector('.storysplat-hotspot-popup-title') as HTMLElement;\n const contentEl = popup.querySelector('.storysplat-hotspot-popup-content') as HTMLElement;\n const closeBtn = popup.querySelector('.storysplat-hotspot-popup-close') as HTMLElement;\n\n // Reset any previous custom styles and classes\n popup.style.cssText = '';\n popup.classList.remove('fullscreen');\n\n // Determine activation mode (default to 'click')\n const activationMode = hotspot.activationMode || 'click';\n const hasMediaContent = hotspot.photoUrl || hotspot.popupVideoUrl || (hotspot.contentType === 'iframe' && hotspot.iframeUrl);\n\n // Apply fullscreen mode for click activation with photo, video, or iframe (matching BabylonJS export)\n if (activationMode === 'click' && hasMediaContent) {\n popup.classList.add('fullscreen');\n }\n\n // Apply custom styling if provided (matching BabylonJS export)\n if (hotspot.backgroundColor) {\n popup.style.backgroundColor = hotspot.backgroundColor;\n }\n if (hotspot.textColor) {\n popup.style.color = hotspot.textColor;\n if (titleEl) titleEl.style.color = hotspot.textColor;\n }\n if (hotspot.fontFamily) {\n popup.style.fontFamily = hotspot.fontFamily;\n }\n if (hotspot.fontSize) {\n popup.style.fontSize = `${hotspot.fontSize}px`;\n }\n\n // Apply close button color if provided\n if (closeBtn && hotspot.closeButtonColor) {\n closeBtn.style.backgroundColor = hotspot.closeButtonColor;\n }\n\n // Set title\n if (titleEl) {\n titleEl.textContent = hotspot.title || 'Hotspot';\n }\n\n // Build content HTML - order matches BabylonJS export: title, iframe, photo, info, link\n let contentHtml = '';\n\n // Iframe content (before photo in BabylonJS export)\n if (hotspot.contentType === 'iframe' && hotspot.iframeUrl) {\n contentHtml += `<iframe src=\"${hotspot.iframeUrl}\" title=\"${hotspot.title || 'Embedded content'}\"></iframe>`;\n }\n\n // Video content (embedded video player in popup)\n if (hotspot.popupVideoUrl) {\n contentHtml += `<video src=\"${hotspot.popupVideoUrl}\" controls playsinline webkit-playsinline preload=\"metadata\"></video>`;\n }\n\n // Image content\n if (hotspot.photoUrl) {\n contentHtml += `<img src=\"${hotspot.photoUrl}\" alt=\"${hotspot.title || 'Hotspot image'}\" />`;\n }\n\n // Information text\n if (hotspot.information) {\n contentHtml += `<p>${hotspot.information}</p>`;\n }\n\n // External link\n if (hotspot.externalLinkUrl) {\n const btnColor = hotspot.externalLinkButtonColor || '#007bff';\n contentHtml += `\n <div onclick=\"window.open('${hotspot.externalLinkUrl}', '_blank', 'noopener,noreferrer')\"\n class=\"storysplat-hotspot-popup-link\" style=\"background-color: ${btnColor}\">\n ${hotspot.externalLinkText || 'Open External Link'}\n </div>\n `;\n }\n\n if (contentEl) {\n contentEl.innerHTML = contentHtml;\n }\n\n // Show popup (no overlay - matching BabylonJS export)\n popup.classList.add('visible');\n}\n\n/**\n * Hide hotspot popup\n */\nexport function hideHotspotPopup(container: HTMLElement): void {\n const popup = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n\n if (popup) popup.classList.remove('visible', 'fullscreen');\n}\n\n/**\n * Show/hide virtual joystick overlay\n */\nexport function setJoystickVisible(elements: UIElements, visible: boolean): void {\n if (elements.joystick) {\n if (visible) {\n elements.joystick.classList.add('visible');\n } else {\n elements.joystick.classList.remove('visible');\n }\n }\n if (elements.lookZone) {\n if (visible) {\n elements.lookZone.classList.add('visible');\n } else {\n elements.lookZone.classList.remove('visible');\n }\n }\n}\n\n/**\n * Update joystick thumb position based on touch input\n */\nexport function updateJoystickPosition(\n elements: UIElements,\n active: boolean,\n dx: number,\n dy: number,\n maxRadius: number\n): void {\n if (!elements.joystickThumb) return;\n\n if (active) {\n elements.joystickThumb.classList.add('active');\n // Clamp the offset to maxRadius\n const distance = Math.sqrt(dx * dx + dy * dy);\n const clampedDistance = Math.min(distance, maxRadius);\n const scale = distance > 0 ? clampedDistance / distance : 0;\n const clampedX = dx * scale;\n const clampedY = dy * scale;\n elements.joystickThumb.style.transform = `translate(${clampedX}px, ${clampedY}px)`;\n } else {\n elements.joystickThumb.classList.remove('active');\n elements.joystickThumb.style.transform = 'translate(0, 0)';\n }\n}\n\n/**\n * Show/hide camera mode toggle button (mobile-only, explore mode only)\n */\nexport function setCameraModeToggleVisible(elements: UIElements, visible: boolean): void {\n if (elements.cameraModeToggle) {\n if (visible) {\n elements.cameraModeToggle.classList.add('visible');\n } else {\n elements.cameraModeToggle.classList.remove('visible');\n }\n }\n}\n\n/**\n * Update camera mode toggle button to reflect current mode\n */\nexport function updateCameraModeToggle(elements: UIElements, mode: 'orbit' | 'fly'): void {\n if (elements.cameraModeToggle) {\n elements.cameraModeToggle.classList.remove('orbit', 'fly');\n elements.cameraModeToggle.classList.add(mode);\n }\n}\n\n/**\n * Show/hide return to waypoint button (explore mode only)\n */\nexport function setReturnWaypointButtonVisible(elements: UIElements, visible: boolean): void {\n if (elements.returnWaypointButton) {\n if (visible) {\n elements.returnWaypointButton.classList.add('visible');\n } else {\n elements.returnWaypointButton.classList.remove('visible');\n }\n }\n}\n\n/**\n * Lazy load UI options\n */\nexport interface LazyLoadUIOptions {\n thumbnailUrl?: string;\n buttonText?: string;\n uiColor?: string;\n onStart: () => void;\n}\n\n/**\n * Create lazy load UI with thumbnail and start button\n * Returns the container element for later removal\n */\nexport function createLazyLoadUI(\n container: HTMLElement,\n options: LazyLoadUIOptions\n): HTMLElement {\n const {\n thumbnailUrl,\n buttonText = 'Start Experience',\n uiColor = '#4CAF50',\n onStart\n } = options;\n\n // Inject styles if not already present\n injectStyles(uiColor);\n\n // Add container class\n container.classList.add('storysplat-viewer-container');\n\n // Create lazy load container\n const lazyLoadContainer = document.createElement('div');\n lazyLoadContainer.className = 'storysplat-lazy-load-container';\n\n // Build HTML content\n let html = '';\n\n // Thumbnail if provided\n if (thumbnailUrl) {\n html += `<img class=\"storysplat-lazy-load-thumbnail\" src=\"${thumbnailUrl}\" alt=\"Scene preview\" />`;\n }\n\n // Overlay gradient\n html += `<div class=\"storysplat-lazy-load-overlay\"></div>`;\n\n // Content with start button\n html += `\n <div class=\"storysplat-lazy-load-content\">\n <button class=\"storysplat-lazy-load-start-btn\" style=\"background: ${uiColor}\">\n <svg viewBox=\"0 0 24 24\">\n <path d=\"M8 5v14l11-7z\"/>\n </svg>\n ${buttonText}\n </button>\n </div>\n `;\n\n lazyLoadContainer.innerHTML = html;\n container.appendChild(lazyLoadContainer);\n\n // Add click handler to start button\n const startBtn = lazyLoadContainer.querySelector('.storysplat-lazy-load-start-btn');\n startBtn?.addEventListener('click', () => {\n // Fade out and remove\n lazyLoadContainer.style.transition = 'opacity 0.3s ease-out';\n lazyLoadContainer.style.opacity = '0';\n setTimeout(() => {\n lazyLoadContainer.remove();\n onStart();\n }, 300);\n });\n\n return lazyLoadContainer;\n}\n","/**\n * CameraControls - PlayCanvas Camera Controller\n *\n * Ported from PlayCanvas engine scripts/esm/camera-controls.mjs\n * Provides orbit, fly, and focus camera modes with mouse, touch, and gamepad support.\n *\n * Controls:\n * - LMB drag: Orbit around focus point\n * - RMB drag + WASD: Fly mode\n * - Shift + drag / MMB: Pan\n * - Scroll / Pinch: Zoom\n * - Double-click: Focus on point (handled externally)\n */\n\nimport * as pc from 'playcanvas';\n\nconst tmpV1 = new pc.Vec3();\nconst tmpV2 = new pc.Vec3();\nconst pose = new pc.Pose();\nconst frame = new pc.InputFrame({\n move: [0, 0, 0],\n rotate: [0, 0, 0]\n});\n\n/**\n * Calculate the damp rate for smooth interpolation\n */\nexport const damp = (damping: number, dt: number): number => 1 - Math.pow(damping, dt * 1000);\n\n/**\n * Apply dead zone to gamepad stick input\n */\nconst applyDeadZone = (stick: number[], low: number, high: number): void => {\n const mag = Math.sqrt(stick[0] * stick[0] + stick[1] * stick[1]);\n if (mag < low) {\n stick.fill(0);\n return;\n }\n const scale = (mag - low) / (high - low);\n stick[0] *= scale / mag;\n stick[1] *= scale / mag;\n};\n\n/**\n * Converts screen space mouse deltas to world space pan vector\n */\nconst screenToWorld = (\n camera: pc.CameraComponent,\n dx: number,\n dy: number,\n dz: number,\n out: pc.Vec3 = new pc.Vec3()\n): pc.Vec3 => {\n const { fov, aspectRatio, horizontalFov, projection, orthoHeight } = camera;\n const app = (camera as any).system?.app;\n const { width, height } = app?.graphicsDevice?.clientRect || { width: 1920, height: 1080 };\n\n // normalize deltas to device coord space\n out.set(\n -(dx / width) * 2,\n (dy / height) * 2,\n 0\n );\n\n // calculate half size of the view frustum at the current distance\n const halfSize = tmpV2.set(0, 0, 0);\n if (projection === pc.PROJECTION_PERSPECTIVE) {\n const halfSlice = dz * Math.tan(0.5 * fov * pc.math.DEG_TO_RAD);\n if (horizontalFov) {\n halfSize.set(halfSlice, halfSlice / aspectRatio, 0);\n } else {\n halfSize.set(halfSlice * aspectRatio, halfSlice, 0);\n }\n } else {\n halfSize.set(orthoHeight * aspectRatio, orthoHeight, 0);\n }\n\n // scale by device coord space\n out.mul(halfSize);\n return out;\n};\n\ntype CameraMode = 'orbit' | 'fly' | 'focus';\ntype MobileInputLayout = 'joystick-joystick' | 'joystick-touch' | 'touch-joystick' | 'touch-touch';\n\nexport interface CameraControlsConfig {\n enableOrbit?: boolean;\n enableFly?: boolean;\n enablePan?: boolean;\n focusPoint?: pc.Vec3;\n moveSpeed?: number;\n moveFastSpeed?: number;\n moveSlowSpeed?: number;\n rotateSpeed?: number;\n rotateTouchSens?: number;\n rotateJoystickSens?: number;\n zoomSpeed?: number;\n zoomPinchSens?: number;\n focusDamping?: number;\n rotateDamping?: number;\n moveDamping?: number;\n zoomDamping?: number;\n pitchRange?: pc.Vec2;\n yawRange?: pc.Vec2;\n zoomRange?: pc.Vec2;\n gamepadDeadZone?: pc.Vec2;\n mobileInputLayout?: MobileInputLayout;\n /** Invert camera rotation (Y-axis) for inverted mouse/touch controls */\n invertRotation?: boolean;\n}\n\n/**\n * CameraControls class - Manages camera with orbit, fly, and focus modes\n */\nexport class CameraControls {\n private camera: pc.Entity;\n private cameraComponent: pc.CameraComponent;\n private app: pc.Application;\n private enabled: boolean = true;\n\n // Mode state\n private _mode: CameraMode = 'orbit';\n private _enableOrbit: boolean = true;\n private _enableFly: boolean = true;\n enablePan: boolean = true;\n\n // Controllers\n private _flyController: pc.FlyController;\n private _orbitController: pc.OrbitController;\n private _focusController: pc.FocusController;\n private _controller: pc.InputController;\n private _pose: pc.Pose = new pc.Pose();\n\n // Input sources\n private _desktopInput: pc.KeyboardMouseSource;\n private _orbitMobileInput: pc.MultiTouchSource;\n private _flyMobileInput: pc.DualGestureSource;\n private _gamepadInput: pc.GamepadSource;\n\n // Zoom\n private _startZoomDist: number = 0;\n // Limit pitch to ±89° to avoid gimbal lock (world flip at ±90°)\n private _pitchRange: pc.Vec2 = new pc.Vec2(-89, 89);\n private _yawRange: pc.Vec2 = new pc.Vec2(-Infinity, Infinity);\n\n // Store the last focus point for orbit mode\n private _lastFocusPoint: pc.Vec3 = new pc.Vec3(0, 0, 0);\n private _zoomRange: pc.Vec2 = new pc.Vec2(0.01, Infinity);\n\n // Debug: track last yaw for discontinuity detection\n private _lastYaw?: number;\n\n // State tracking\n private _state = {\n axis: new pc.Vec3(),\n shift: 0,\n ctrl: 0,\n mouse: [0, 0, 0],\n touches: 0\n };\n\n // Speed settings\n moveSpeed: number = 25;\n moveFastSpeed: number = 50;\n moveSlowSpeed: number = 10;\n rotateSpeed: number = 0.05;\n rotateTouchSens: number = 0.1; // Touch sensitivity (10x lower than desktop)\n rotateJoystickSens: number = 1;\n zoomSpeed: number = 0.001;\n zoomPinchSens: number = 5;\n keyboardSpeedMultiplier: number = 1.5; // Desktop keyboard moves faster than mobile joystick\n gamepadDeadZone: pc.Vec2 = new pc.Vec2(0.3, 0.6);\n /** Invert camera rotation (Y-axis) */\n invertRotation: boolean = false;\n\n // Joystick event name for UI\n joystickEventName: string = 'joystick';\n\n // Cleanup handlers\n private _destroyHandler: (() => void) | null = null;\n\n constructor(camera: pc.Entity, app: pc.Application, config: CameraControlsConfig = {}) {\n this.camera = camera;\n this.app = app;\n\n const cameraComponent = camera.camera;\n if (!cameraComponent) {\n throw new Error('CameraControls: camera component not found on entity');\n }\n this.cameraComponent = cameraComponent;\n\n // Initialize controllers\n this._flyController = new pc.FlyController();\n this._orbitController = new pc.OrbitController();\n this._focusController = new pc.FocusController();\n\n // Set default dampening values for smooth BabylonJS-like feel\n this._flyController.moveDamping = 0.9;\n this._flyController.rotateDamping = 0.9;\n this._orbitController.rotateDamping = 0.9;\n this._orbitController.zoomDamping = 0.9;\n\n // Set orbit controller defaults\n this._orbitController.zoomRange = new pc.Vec2(0.01, Infinity);\n\n // Initialize input sources\n const canvas = app.graphicsDevice.canvas;\n this._desktopInput = new pc.KeyboardMouseSource();\n this._orbitMobileInput = new pc.MultiTouchSource();\n this._flyMobileInput = new pc.DualGestureSource();\n this._gamepadInput = new pc.GamepadSource();\n\n // Attach input sources to canvas\n this._desktopInput.attach(canvas);\n this._orbitMobileInput.attach(canvas);\n this._flyMobileInput.attach(canvas);\n this._gamepadInput.attach(canvas);\n\n // Expose UI joystick events\n this._flyMobileInput.on('joystick:position:left', ([bx, by, sx, sy]: number[]) => {\n if (this._mode !== 'fly') return;\n this.app.fire(`${this.joystickEventName}:left`, bx, by, sx, sy);\n });\n this._flyMobileInput.on('joystick:position:right', ([bx, by, sx, sy]: number[]) => {\n if (this._mode !== 'fly') return;\n this.app.fire(`${this.joystickEventName}:right`, bx, by, sx, sy);\n });\n\n // Initialize pose from camera position\n this._pose.look(this.camera.getPosition(), pc.Vec3.ZERO);\n\n // Set initial mode\n this._setMode('orbit');\n\n // Store controller reference\n this._controller = this._orbitController;\n\n // Apply config\n if (config.enableOrbit !== undefined) this.enableOrbit = config.enableOrbit;\n if (config.enableFly !== undefined) this.enableFly = config.enableFly;\n if (config.enablePan !== undefined) this.enablePan = config.enablePan;\n if (config.focusPoint) this.focusPoint = config.focusPoint;\n if (config.moveSpeed !== undefined) this.moveSpeed = config.moveSpeed;\n if (config.moveFastSpeed !== undefined) this.moveFastSpeed = config.moveFastSpeed;\n if (config.moveSlowSpeed !== undefined) this.moveSlowSpeed = config.moveSlowSpeed;\n if (config.rotateSpeed !== undefined) this.rotateSpeed = config.rotateSpeed;\n if (config.rotateTouchSens !== undefined) this.rotateTouchSens = config.rotateTouchSens;\n if (config.rotateJoystickSens !== undefined) this.rotateJoystickSens = config.rotateJoystickSens;\n if (config.zoomSpeed !== undefined) this.zoomSpeed = config.zoomSpeed;\n if (config.zoomPinchSens !== undefined) this.zoomPinchSens = config.zoomPinchSens;\n if (config.focusDamping !== undefined) this.focusDamping = config.focusDamping;\n if (config.rotateDamping !== undefined) this.rotateDamping = config.rotateDamping;\n if (config.moveDamping !== undefined) this.moveDamping = config.moveDamping;\n if (config.zoomDamping !== undefined) this.zoomDamping = config.zoomDamping;\n if (config.pitchRange) this.pitchRange = config.pitchRange;\n if (config.yawRange) this.yawRange = config.yawRange;\n if (config.zoomRange) this.zoomRange = config.zoomRange;\n if (config.gamepadDeadZone) this.gamepadDeadZone = config.gamepadDeadZone;\n if (config.mobileInputLayout) this.mobileInputLayout = config.mobileInputLayout;\n if (config.invertRotation !== undefined) this.invertRotation = config.invertRotation;\n }\n\n // Enable/disable getters and setters\n set enableFly(enable: boolean) {\n this._enableFly = enable;\n if (!this._enableFly && this._mode === 'fly') {\n this._setMode('orbit');\n }\n }\n\n get enableFly(): boolean {\n return this._enableFly;\n }\n\n set enableOrbit(enable: boolean) {\n this._enableOrbit = enable;\n if (!this._enableOrbit && this._mode === 'orbit') {\n this._setMode('fly');\n }\n }\n\n get enableOrbit(): boolean {\n return this._enableOrbit;\n }\n\n // Damping getters/setters\n set focusDamping(damping: number) {\n this._focusController.focusDamping = damping;\n }\n\n get focusDamping(): number {\n return this._focusController.focusDamping;\n }\n\n set moveDamping(damping: number) {\n this._flyController.moveDamping = damping;\n }\n\n get moveDamping(): number {\n return this._flyController.moveDamping;\n }\n\n set rotateDamping(damping: number) {\n this._flyController.rotateDamping = damping;\n this._orbitController.rotateDamping = damping;\n }\n\n get rotateDamping(): number {\n return this._orbitController.rotateDamping;\n }\n\n set zoomDamping(damping: number) {\n this._orbitController.zoomDamping = damping;\n }\n\n get zoomDamping(): number {\n return this._orbitController.zoomDamping;\n }\n\n // Focus point getter/setter\n set focusPoint(point: pc.Vec3) {\n const position = this.camera.getPosition();\n this._startZoomDist = position.distance(point);\n this._controller.attach(this._pose.look(position, point), false);\n }\n\n get focusPoint(): pc.Vec3 {\n return this._pose.getFocus(tmpV1);\n }\n\n // Range getters/setters\n set pitchRange(range: pc.Vec2) {\n this._pitchRange.copy(range);\n this._flyController.pitchRange = this._pitchRange;\n this._orbitController.pitchRange = this._pitchRange;\n }\n\n get pitchRange(): pc.Vec2 {\n return this._pitchRange;\n }\n\n set yawRange(range: pc.Vec2) {\n this._yawRange.x = pc.math.clamp(range.x, -360, 360);\n this._yawRange.y = pc.math.clamp(range.y, -360, 360);\n this._flyController.yawRange = this._yawRange;\n this._orbitController.yawRange = this._yawRange;\n }\n\n get yawRange(): pc.Vec2 {\n return this._yawRange;\n }\n\n set zoomRange(range: pc.Vec2) {\n this._zoomRange.x = range.x;\n this._zoomRange.y = range.y <= range.x ? Infinity : range.y;\n this._orbitController.zoomRange = this._zoomRange;\n }\n\n get zoomRange(): pc.Vec2 {\n return this._zoomRange;\n }\n\n // Mobile input layout\n set mobileInputLayout(layout: MobileInputLayout) {\n if (!/(?:joystick|touch)-(?:joystick|touch)/.test(layout)) {\n console.warn(`CameraControls: invalid mobile input layout: ${layout}`);\n return;\n }\n this._flyMobileInput.layout = layout;\n }\n\n get mobileInputLayout(): MobileInputLayout {\n return this._flyMobileInput.layout as MobileInputLayout;\n }\n\n /**\n * Get current camera mode\n */\n get mode(): CameraMode {\n return this._mode;\n }\n\n /**\n * Set camera mode\n */\n private _setMode(mode: CameraMode): void {\n // Override mode depending on enabled features\n if (this._enableFly && !this._enableOrbit) {\n mode = 'fly';\n } else if (!this._enableFly && this._enableOrbit) {\n mode = 'orbit';\n } else if (!this._enableFly && !this._enableOrbit) {\n console.warn('CameraControls: both fly and orbit modes are disabled');\n return;\n }\n\n const previousMode = this._mode;\n if (previousMode === mode) return;\n this._mode = mode;\n\n // Detach old controller\n if (this._controller) {\n this._controller.detach();\n }\n\n // Attach new controller\n switch (this._mode) {\n case 'orbit':\n this._controller = this._orbitController;\n // When switching to orbit (especially after focus), set up orbit around the last focus point\n if (previousMode === 'focus') {\n const position = this.camera.getPosition();\n this._pose.look(position, this._lastFocusPoint);\n }\n // Reset yaw tracking to prevent false discontinuity detection on mode switch\n this._lastYaw = this._pose.angles.y;\n break;\n case 'fly':\n this._controller = this._flyController;\n break;\n case 'focus':\n this._controller = this._focusController;\n break;\n }\n this._controller.attach(this._pose, false);\n\n // Emit mode change event for UI updates\n this.app.fire('cameracontrols:modechange', this._mode);\n }\n\n /**\n * Public method to set camera mode (orbit or fly only)\n * Used by UI toggle buttons to switch between orbit and fly modes\n */\n setMode(mode: 'orbit' | 'fly'): void {\n this._setMode(mode);\n }\n\n /**\n * Focus camera on a point with smooth animation.\n * After the focus animation completes, orbit mode will use this point as the orbit center.\n */\n focus(focusPoint: pc.Vec3, resetZoom: boolean = false): void {\n // Store the focus point so orbit mode uses it as the center\n this._lastFocusPoint.copy(focusPoint);\n\n this._setMode('focus');\n const zoomDist = resetZoom\n ? this._startZoomDist\n : this.camera.getPosition().distance(focusPoint);\n this._startZoomDist = zoomDist; // Update the zoom distance for orbit mode\n const position = tmpV1.copy(this.camera.forward).mulScalar(-zoomDist).add(focusPoint);\n this._controller.attach(pose.look(position, focusPoint));\n }\n\n /**\n * Look at a point without moving\n */\n look(focusPoint: pc.Vec3, resetZoom: boolean = false): void {\n this._setMode('focus');\n const position = resetZoom\n ? tmpV1.copy(this.camera.getPosition()).sub(focusPoint).normalize().mulScalar(this._startZoomDist).add(focusPoint)\n : this.camera.getPosition();\n this._controller.attach(pose.look(position, focusPoint));\n }\n\n /**\n * Reset camera to a position looking at focus point\n */\n reset(focusPoint: pc.Vec3, position: pc.Vec3): void {\n this._setMode('focus');\n this._controller.attach(pose.look(position, focusPoint));\n }\n\n /**\n * Sync internal pose from camera's current position and rotation.\n * If a target is provided, the pose is set up to look at that target from the current position.\n * Otherwise, preserves the camera's exact rotation.\n */\n syncFromCamera(target?: pc.Vec3): void {\n const position = this.camera.getPosition().clone();\n\n if (target) {\n // When we have a target, use pose.look() to properly set up the pose\n // This calculates correct angles to look at the target from current position\n this._lastFocusPoint.copy(target);\n const focusDistance = position.distance(target);\n this._startZoomDist = focusDistance;\n this._pose.distance = focusDistance;\n\n // Use pose.look() which properly calculates yaw/pitch to face the target\n this._pose.look(position, target);\n\n // Normalize yaw to [-180, 180] range\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n\n // Clamp pitch to prevent flipping (avoid gimbal lock near ±90°)\n this._pose.angles.x = Math.max(-89, Math.min(89, this._pose.angles.x));\n\n // Ensure no roll\n this._pose.angles.z = 0;\n } else {\n // No target - preserve camera's exact rotation\n const eulerAngles = this.camera.getEulerAngles();\n\n // Set pose position directly from camera\n this._pose.position.copy(position);\n\n // Set pose angles directly from camera's euler angles\n this._pose.angles.x = eulerAngles.x;\n this._pose.angles.y = eulerAngles.y;\n this._pose.angles.z = 0; // Always zero roll to prevent flipping\n\n // Normalize yaw to [-180, 180] range\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n\n // Clamp pitch to prevent flipping\n this._pose.angles.x = Math.max(-89, Math.min(89, this._pose.angles.x));\n\n // Calculate a focus point 10 units in front of the camera\n const forward = this.camera.forward.clone();\n this._lastFocusPoint.copy(position).add(forward.mulScalar(10));\n\n const focusDistance = 10;\n this._startZoomDist = focusDistance;\n this._pose.distance = focusDistance;\n }\n\n // Reattach the current controller with the synced pose\n this._controller.attach(this._pose, false);\n }\n\n /**\n * Sync internal pose from explicit position and rotation values.\n * Use this when you need to bypass the camera entity's current state.\n * Also sets the camera entity to match these values.\n */\n syncFromPose(position: pc.Vec3, rotation: pc.Quat, focusTarget?: pc.Vec3): void {\n // Set the camera entity to match the provided pose\n this.camera.setPosition(position);\n this.camera.setRotation(rotation);\n\n // Get euler angles from the quaternion\n const tempEntity = new pc.Entity();\n tempEntity.setRotation(rotation);\n const eulerAngles = tempEntity.getEulerAngles();\n\n // Set pose position\n this._pose.position.copy(position);\n\n // Set pose angles from the rotation\n this._pose.angles.x = eulerAngles.x;\n this._pose.angles.y = eulerAngles.y;\n this._pose.angles.z = 0; // Always zero roll\n\n // Normalize yaw to [-180, 180] range\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n\n // Clamp pitch to prevent flipping\n this._pose.angles.x = Math.max(-89, Math.min(89, this._pose.angles.x));\n\n // Calculate focus point\n if (focusTarget) {\n this._lastFocusPoint.copy(focusTarget);\n } else {\n // Calculate a focus point 10 units in front\n const forward = this.camera.forward.clone();\n this._lastFocusPoint.copy(position).add(forward.mulScalar(10));\n }\n\n const focusDistance = position.distance(this._lastFocusPoint);\n this._startZoomDist = focusDistance;\n this._pose.distance = focusDistance;\n\n // Reattach the current controller with the synced pose\n this._controller.attach(this._pose, false);\n }\n\n /**\n * Enable controls\n */\n enable(): void {\n this.enabled = true;\n }\n\n /**\n * Disable controls\n */\n disable(): void {\n this.enabled = false;\n // Discard any pending inputs\n this._desktopInput.read();\n this._orbitMobileInput.read();\n this._flyMobileInput.read();\n this._gamepadInput.read();\n }\n\n /**\n * Update camera controls - call this every frame\n */\n update(dt: number): void {\n if (!this.enabled) return;\n\n const { keyCode } = pc.KeyboardMouseSource;\n const { key, button, mouse, wheel } = this._desktopInput.read();\n const { touch, pinch, count } = this._orbitMobileInput.read();\n const { leftInput, rightInput } = this._flyMobileInput.read();\n const { leftStick, rightStick } = this._gamepadInput.read();\n\n // Apply dead zone to gamepad sticks\n applyDeadZone(leftStick, this.gamepadDeadZone.x, this.gamepadDeadZone.y);\n applyDeadZone(rightStick, this.gamepadDeadZone.x, this.gamepadDeadZone.y);\n\n // Update state\n this._state.axis.add(tmpV1.set(\n (key[keyCode.D] - key[keyCode.A]) + (key[keyCode.RIGHT] - key[keyCode.LEFT]),\n (key[keyCode.E] - key[keyCode.Q]),\n (key[keyCode.W] - key[keyCode.S]) + (key[keyCode.UP] - key[keyCode.DOWN])\n ));\n for (let i = 0; i < this._state.mouse.length; i++) {\n this._state.mouse[i] += button[i];\n }\n this._state.shift += key[keyCode.SHIFT];\n this._state.ctrl += key[keyCode.CTRL];\n this._state.touches += count[0];\n\n // Mode switching based on input\n if (button[0] === 1 || button[1] === 1 || wheel[0] !== 0) {\n // Left mouse button, middle mouse button, mouse wheel -> orbit\n this._setMode('orbit');\n } else if (button[2] === 1 || this._state.axis.length() > 0) {\n // Right mouse button or any movement -> fly\n this._setMode('fly');\n }\n\n const orbit = +(this._mode === 'orbit');\n const fly = +(this._mode === 'fly');\n const double = +(this._state.touches > 1);\n const desktopPan = +(this._state.shift || this._state.mouse[1]);\n const mobileJoystick = +(this._flyMobileInput.layout.endsWith('joystick'));\n\n // Multipliers\n const moveMult = (this._state.shift ? this.moveFastSpeed : this._state.ctrl\n ? this.moveSlowSpeed : this.moveSpeed) * dt;\n const zoomMult = this.zoomSpeed * 60 * dt;\n const zoomTouchMult = zoomMult * this.zoomPinchSens;\n const rotateMult = this.rotateSpeed * 60 * dt;\n const rotateTouchMult = rotateMult * this.rotateTouchSens; // Touch-specific rotation (10x lower)\n const rotateJoystickMult = this.rotateSpeed * this.rotateJoystickSens * 60 * dt;\n\n const { deltas } = frame;\n\n // Desktop move\n const v = tmpV1.set(0, 0, 0);\n const keyMove = this._state.axis.clone().normalize();\n v.add(keyMove.mulScalar(fly * moveMult * this.keyboardSpeedMultiplier));\n const panMove = screenToWorld(this.cameraComponent, mouse[0], mouse[1], this._pose.distance);\n v.add(panMove.mulScalar(orbit * desktopPan * +this.enablePan));\n const wheelMove = tmpV2.set(0, 0, wheel[0]);\n v.add(wheelMove.mulScalar(orbit * zoomMult));\n deltas.move.append([v.x, v.y, v.z]);\n\n // Desktop rotate\n v.set(0, 0, 0);\n const mouseRotate = tmpV2.set(mouse[0], mouse[1], 0);\n v.add(mouseRotate.mulScalar((1 - (orbit * desktopPan)) * rotateMult));\n // Apply inversion if enabled (inverts Y/pitch axis)\n if (this.invertRotation) v.y = -v.y;\n deltas.rotate.append([v.x, v.y, v.z]);\n\n // Mobile move\n v.set(0, 0, 0);\n const flyMove = tmpV2.set(leftInput[0], 0, -leftInput[1]);\n v.add(flyMove.mulScalar(fly * moveMult));\n const orbitMove = screenToWorld(this.cameraComponent, touch[0], touch[1], this._pose.distance);\n v.add(orbitMove.mulScalar(orbit * double * +this.enablePan));\n const pinchMove = tmpV2.set(0, 0, pinch[0]);\n v.add(pinchMove.mulScalar(orbit * double * zoomTouchMult));\n deltas.move.append([v.x, v.y, v.z]);\n\n // Mobile rotate (uses rotateTouchMult for reduced sensitivity on touch devices)\n v.set(0, 0, 0);\n const orbitRotate = tmpV2.set(touch[0], touch[1], 0);\n v.add(orbitRotate.mulScalar(orbit * (1 - double) * rotateTouchMult));\n const flyRotate = tmpV2.set(rightInput[0], rightInput[1], 0);\n v.add(flyRotate.mulScalar(fly * (mobileJoystick ? rotateJoystickMult : rotateTouchMult)));\n // Apply inversion if enabled (inverts Y/pitch axis)\n if (this.invertRotation) v.y = -v.y;\n deltas.rotate.append([v.x, v.y, v.z]);\n\n // Gamepad move\n v.set(0, 0, 0);\n const stickMove = tmpV2.set(leftStick[0], 0, -leftStick[1]);\n v.add(stickMove.mulScalar(fly * moveMult));\n deltas.move.append([v.x, v.y, v.z]);\n\n // Gamepad rotate\n v.set(0, 0, 0);\n const stickRotate = tmpV2.set(rightStick[0], rightStick[1], 0);\n v.add(stickRotate.mulScalar(fly * rotateJoystickMult));\n // Apply inversion if enabled (inverts Y/pitch axis)\n if (this.invertRotation) v.y = -v.y;\n deltas.rotate.append([v.x, v.y, v.z]);\n\n // Check if XR is active - discard frame if so\n if ((this.app as any).xr?.active) {\n frame.read();\n return;\n }\n\n // Check focus end\n if (this._mode === 'focus') {\n const focusInterrupt = deltas.move.length() + deltas.rotate.length() > 0;\n const focusComplete = (this._focusController as any).complete?.() ?? false;\n if (focusInterrupt || focusComplete) {\n this._setMode('orbit');\n }\n }\n\n // Update controller by consuming frame\n this._pose.copy(this._controller.update(frame, dt));\n\n // Fix yaw discontinuity in orbit mode\n // PlayCanvas's Pose.lerp() uses `% 360` after lerpAngle which doesn't handle\n // negative angles correctly (e.g., -185 % 360 = -185, not 175)\n // This causes sudden jumps when yaw crosses the ±180° boundary\n if (this._mode === 'orbit') {\n // Normalize yaw to [-180, 180) range to prevent accumulation issues\n let yaw = this._pose.angles.y;\n while (yaw > 180) yaw -= 360;\n while (yaw < -180) yaw += 360;\n\n if (this._lastYaw !== undefined) {\n // Check for discontinuous jump (more than 90° in a single frame is unnatural)\n const yawDelta = yaw - this._lastYaw;\n\n // If there's a large jump, adjust to maintain continuity\n if (yawDelta > 180) {\n yaw -= 360;\n } else if (yawDelta < -180) {\n yaw += 360;\n }\n }\n\n this._pose.angles.y = yaw;\n this._lastYaw = yaw;\n }\n\n this.camera.setPosition(this._pose.position);\n this.camera.setEulerAngles(this._pose.angles);\n }\n\n /**\n * Clean up resources\n */\n destroy(): void {\n this._desktopInput.destroy();\n this._orbitMobileInput.destroy();\n this._flyMobileInput.destroy();\n this._gamepadInput.destroy();\n\n this._flyController.destroy();\n this._orbitController.destroy();\n }\n}\n","/**\n * CharacterController - First Person Character Controller with Collisions\n *\n * A lightweight character controller for PlayCanvas that provides:\n * - WASD/Arrow key movement\n * - Mouse look\n * - Gravity and ground detection via raycasting\n * - Collision detection with scene meshes via raycasting\n *\n * Does NOT require ammo.js physics - uses simple raycasting for collisions.\n */\n\nimport * as pc from 'playcanvas';\n\n/**\n * Calculate the damp rate for smooth interpolation\n * Same formula as PlayCanvas's camera-controls for consistent feel\n */\nconst damp = (damping: number, dt: number): number => 1 - Math.pow(damping, dt * 1000);\n\nexport interface CharacterControllerConfig {\n /** Movement speed in units per second */\n moveSpeed?: number;\n /** Sprint speed multiplier */\n sprintMultiplier?: number;\n /** Mouse look sensitivity */\n lookSensitivity?: number;\n /** Player height (camera Y offset from ground) */\n playerHeight?: number;\n /** Gravity strength (units per second squared) */\n gravity?: number;\n /** Maximum fall speed */\n maxFallSpeed?: number;\n /** Jump velocity */\n jumpVelocity?: number;\n /** Collision radius for horizontal movement */\n collisionRadius?: number;\n /** Step height for climbing small obstacles */\n stepHeight?: number;\n /** Ground check distance below feet */\n groundCheckDistance?: number;\n /** Movement dampening for smooth start/stop (0-1, higher = more smoothing) */\n moveDamping?: number;\n}\n\ninterface CollisionMeshData {\n entity: pc.Entity;\n meshType: string;\n position: number[];\n rotation: number[];\n scaling: number[];\n}\n\n/**\n * CharacterController class - First person controller with collision detection\n */\nexport class CharacterController {\n private camera: pc.Entity;\n private app: pc.Application;\n private enabled: boolean = false;\n\n // Movement state\n private velocity: pc.Vec3 = new pc.Vec3();\n private isGrounded: boolean = false;\n private yaw: number = 0;\n private pitch: number = 0;\n\n // Input state\n private keys: { [key: string]: boolean } = {};\n private mouseLocked: boolean = false;\n\n // Configuration\n moveSpeed: number = 8; // Increased for faster movement\n sprintMultiplier: number = 2;\n lookSensitivity: number = 0.002;\n playerHeight: number = 1.6;\n gravity: number = 20;\n maxFallSpeed: number = 50;\n jumpVelocity: number = 8;\n collisionRadius: number = 0.3;\n stepHeight: number = 0.3;\n groundCheckDistance: number = 0.1;\n moveDamping: number = 0.9; // Smooth movement like BabylonJS\n\n // Velocity smoothing for gradual start/stop\n private horizontalVelocity: pc.Vec3 = new pc.Vec3();\n private targetVelocity: pc.Vec3 = new pc.Vec3();\n\n // Collision meshes\n private collisionEntities: pc.Entity[] = [];\n private floorEntity: pc.Entity | null = null;\n\n // Event handlers (for cleanup)\n private keydownHandler: ((e: KeyboardEvent) => void) | null = null;\n private keyupHandler: ((e: KeyboardEvent) => void) | null = null;\n private mousemoveHandler: ((e: MouseEvent) => void) | null = null;\n private clickHandler: ((e: MouseEvent) => void) | null = null;\n private pointerlockchangeHandler: (() => void) | null = null;\n\n // Temporary vectors for calculations\n private tmpVec = new pc.Vec3();\n private tmpVec2 = new pc.Vec3();\n private forward = new pc.Vec3();\n private right = new pc.Vec3();\n\n constructor(camera: pc.Entity, app: pc.Application, config: CharacterControllerConfig = {}) {\n this.camera = camera;\n this.app = app;\n\n // Apply config\n if (config.moveSpeed !== undefined) this.moveSpeed = config.moveSpeed;\n if (config.sprintMultiplier !== undefined) this.sprintMultiplier = config.sprintMultiplier;\n if (config.lookSensitivity !== undefined) this.lookSensitivity = config.lookSensitivity;\n if (config.playerHeight !== undefined) this.playerHeight = config.playerHeight;\n if (config.gravity !== undefined) this.gravity = config.gravity;\n if (config.maxFallSpeed !== undefined) this.maxFallSpeed = config.maxFallSpeed;\n if (config.jumpVelocity !== undefined) this.jumpVelocity = config.jumpVelocity;\n if (config.collisionRadius !== undefined) this.collisionRadius = config.collisionRadius;\n if (config.stepHeight !== undefined) this.stepHeight = config.stepHeight;\n if (config.groundCheckDistance !== undefined) this.groundCheckDistance = config.groundCheckDistance;\n if (config.moveDamping !== undefined) this.moveDamping = config.moveDamping;\n\n // Initialize yaw/pitch from camera rotation\n const angles = this.camera.getEulerAngles();\n this.pitch = angles.x;\n this.yaw = angles.y;\n }\n\n /**\n * Create collision meshes from scene data\n */\n async createCollisionMeshes(collisionMeshesData: any[]): Promise<void> {\n if (!collisionMeshesData || collisionMeshesData.length === 0) return;\n\n console.log('[CharacterController] Creating collision meshes:', collisionMeshesData.length);\n\n const loadPromises: Promise<void>[] = [];\n\n collisionMeshesData.forEach((data, index) => {\n // Handle custom mesh (GLB/GLTF files)\n if (data.meshType === 'custom' && data.customMeshUrl) {\n const promise = this.loadCustomCollisionMesh(data, index);\n loadPromises.push(promise);\n return;\n }\n\n let entity: pc.Entity | null = null;\n\n switch (data.meshType) {\n case 'cube':\n entity = new pc.Entity(`collision-cube-${index}`);\n entity.addComponent('render', {\n type: 'box'\n });\n break;\n case 'sphere':\n entity = new pc.Entity(`collision-sphere-${index}`);\n entity.addComponent('render', {\n type: 'sphere'\n });\n break;\n case 'floor':\n entity = new pc.Entity(`collision-floor-${index}`);\n entity.addComponent('render', {\n type: 'plane'\n });\n this.floorEntity = entity;\n break;\n case 'plane':\n default:\n entity = new pc.Entity(`collision-plane-${index}`);\n entity.addComponent('render', {\n type: 'plane'\n });\n break;\n }\n\n if (entity) {\n this.configureCollisionEntity(entity, data);\n }\n });\n\n // Wait for all custom meshes to load\n if (loadPromises.length > 0) {\n await Promise.all(loadPromises);\n }\n\n console.log('[CharacterController] Created', this.collisionEntities.length, 'collision entities');\n }\n\n /**\n * Load a custom collision mesh (GLB/GLTF)\n */\n private async loadCustomCollisionMesh(data: any, index: number): Promise<void> {\n const url = data.customMeshUrl;\n console.log('[CharacterController] Loading custom collision mesh:', url);\n\n try {\n // Determine asset type from URL\n const ext = url.split('?')[0].split('.').pop()?.toLowerCase() || 'glb';\n const assetType = (ext === 'gltf' || ext === 'glb') ? 'container' : 'model';\n\n const asset = new pc.Asset(`collision-custom-${index}`, assetType, { url });\n\n await new Promise<void>((resolve, reject) => {\n asset.ready(() => {\n try {\n const entity = new pc.Entity(`collision-custom-${index}`);\n\n if (assetType === 'container') {\n // GLB/GLTF container - instantiate the model\n const containerResource = asset.resource as pc.ContainerResource;\n if (containerResource && containerResource.instantiateRenderEntity) {\n const renderEntity = containerResource.instantiateRenderEntity();\n // Re-parent all children to our collision entity\n while (renderEntity.children.length > 0) {\n entity.addChild(renderEntity.children[0]);\n }\n renderEntity.destroy();\n }\n } else {\n // Regular model\n entity.addComponent('model', {\n asset: asset\n });\n }\n\n this.configureCollisionEntity(entity, data);\n\n // Store bounding box for custom mesh collision\n this.computeAndStoreBounds(entity);\n\n resolve();\n } catch (err) {\n console.error('[CharacterController] Error setting up custom mesh:', err);\n reject(err);\n }\n });\n\n asset.on('error', (err: any) => {\n console.error('[CharacterController] Error loading custom mesh:', url, err);\n reject(err);\n });\n\n this.app.assets.add(asset);\n this.app.assets.load(asset);\n });\n } catch (error) {\n console.error('[CharacterController] Failed to load custom collision mesh:', url, error);\n }\n }\n\n /**\n * Compute and store bounding box for a collision entity\n */\n private computeAndStoreBounds(entity: pc.Entity): void {\n // Traverse entity and its children to compute combined bounds\n const bounds = new pc.BoundingBox();\n let boundsInitialized = false;\n\n const traverse = (node: pc.Entity) => {\n if (node.render && node.render.meshInstances) {\n for (const mi of node.render.meshInstances) {\n if (mi.aabb) {\n if (!boundsInitialized) {\n bounds.copy(mi.aabb);\n boundsInitialized = true;\n } else {\n bounds.add(mi.aabb);\n }\n }\n }\n }\n for (const child of node.children) {\n if (child instanceof pc.Entity) {\n traverse(child);\n }\n }\n };\n\n traverse(entity);\n\n if (boundsInitialized) {\n (entity as any)._collisionBounds = bounds;\n console.log('[CharacterController] Computed bounds for custom mesh:', bounds.halfExtents);\n }\n }\n\n /**\n * Configure a collision entity with transform and visibility\n */\n private configureCollisionEntity(entity: pc.Entity, data: any): void {\n // Apply transform - convert from BabylonJS coordinates\n const pos = data.position || [0, 0, 0];\n entity.setPosition(pos[0], pos[1], -pos[2]); // Negate Z\n\n const rot = data.rotation || [0, 0, 0];\n entity.setEulerAngles(\n rot[0] * (180 / Math.PI),\n rot[1] * (180 / Math.PI),\n -rot[2] * (180 / Math.PI)\n );\n\n const scale = data.scaling || [1, 1, 1];\n entity.setLocalScale(scale[0], scale[1], scale[2]);\n\n // Make invisible but keep for collision\n this.setEntityVisibility(entity, false);\n\n // Store mesh type for collision logic\n (entity as any)._collisionMeshType = data.meshType;\n\n this.app.root.addChild(entity);\n this.collisionEntities.push(entity);\n }\n\n /**\n * Set visibility of entity and all children\n */\n private setEntityVisibility(entity: pc.Entity, visible: boolean): void {\n if (entity.render) {\n entity.render.enabled = visible;\n }\n for (const child of entity.children) {\n if (child instanceof pc.Entity) {\n this.setEntityVisibility(child, visible);\n }\n }\n }\n\n /**\n * Enable the character controller\n */\n enable(): void {\n if (this.enabled) return;\n this.enabled = true;\n\n // Sync yaw/pitch from current camera rotation\n const angles = this.camera.getEulerAngles();\n this.pitch = angles.x;\n this.yaw = angles.y;\n\n // Reset velocities\n this.velocity.set(0, 0, 0);\n this.horizontalVelocity.set(0, 0, 0);\n this.targetVelocity.set(0, 0, 0);\n\n // Setup input handlers\n this.setupInputHandlers();\n\n console.log('[CharacterController] Enabled');\n }\n\n /**\n * Disable the character controller\n */\n disable(): void {\n if (!this.enabled) return;\n this.enabled = false;\n\n // Remove input handlers\n this.removeInputHandlers();\n\n // Exit pointer lock\n if (document.pointerLockElement) {\n document.exitPointerLock();\n }\n\n console.log('[CharacterController] Disabled');\n }\n\n /**\n * Setup keyboard and mouse input handlers\n */\n private setupInputHandlers(): void {\n const canvas = this.app.graphicsDevice.canvas as HTMLCanvasElement;\n\n // Keyboard handlers\n this.keydownHandler = (e: KeyboardEvent) => {\n this.keys[e.code] = true;\n // Jump on space\n if (e.code === 'Space' && this.isGrounded) {\n this.velocity.y = this.jumpVelocity;\n this.isGrounded = false;\n }\n };\n\n this.keyupHandler = (e: KeyboardEvent) => {\n this.keys[e.code] = false;\n };\n\n // Mouse look handler\n this.mousemoveHandler = (e: MouseEvent) => {\n if (!this.mouseLocked) return;\n\n this.yaw -= e.movementX * this.lookSensitivity * 100;\n this.pitch -= e.movementY * this.lookSensitivity * 100;\n\n // Clamp pitch\n this.pitch = Math.max(-89, Math.min(89, this.pitch));\n };\n\n // Click to lock pointer\n this.clickHandler = () => {\n if (!this.mouseLocked) {\n canvas.requestPointerLock();\n }\n };\n\n // Pointer lock change handler\n this.pointerlockchangeHandler = () => {\n this.mouseLocked = document.pointerLockElement === canvas;\n };\n\n // Add event listeners\n document.addEventListener('keydown', this.keydownHandler);\n document.addEventListener('keyup', this.keyupHandler);\n document.addEventListener('mousemove', this.mousemoveHandler);\n canvas.addEventListener('click', this.clickHandler);\n document.addEventListener('pointerlockchange', this.pointerlockchangeHandler);\n }\n\n /**\n * Remove input handlers\n */\n private removeInputHandlers(): void {\n const canvas = this.app.graphicsDevice.canvas as HTMLCanvasElement;\n\n if (this.keydownHandler) {\n document.removeEventListener('keydown', this.keydownHandler);\n }\n if (this.keyupHandler) {\n document.removeEventListener('keyup', this.keyupHandler);\n }\n if (this.mousemoveHandler) {\n document.removeEventListener('mousemove', this.mousemoveHandler);\n }\n if (this.clickHandler) {\n canvas.removeEventListener('click', this.clickHandler);\n }\n if (this.pointerlockchangeHandler) {\n document.removeEventListener('pointerlockchange', this.pointerlockchangeHandler);\n }\n\n this.keys = {};\n }\n\n /**\n * Check if position collides with any collision mesh\n */\n private checkCollision(position: pc.Vec3, radius: number): boolean {\n // AABB collision check against collision entities\n for (const entity of this.collisionEntities) {\n const entityPos = entity.getPosition();\n const entityScale = entity.getLocalScale();\n const meshType = (entity as any)._collisionMeshType;\n\n if (meshType === 'floor' || meshType === 'plane') {\n // Skip floor for horizontal collision\n continue;\n }\n\n let halfWidth: number, halfHeight: number, halfDepth: number;\n\n // Check if entity has custom bounds (for custom meshes)\n const customBounds = (entity as any)._collisionBounds as pc.BoundingBox | undefined;\n if (customBounds) {\n // Use the actual mesh bounds, scaled by entity scale\n halfWidth = customBounds.halfExtents.x * entityScale.x;\n halfHeight = customBounds.halfExtents.y * entityScale.y;\n halfDepth = customBounds.halfExtents.z * entityScale.z;\n } else {\n // Use entity scale for primitive shapes\n halfWidth = entityScale.x / 2;\n halfHeight = entityScale.y / 2;\n halfDepth = entityScale.z / 2;\n }\n\n const dx = Math.abs(position.x - entityPos.x);\n const dy = Math.abs(position.y - entityPos.y);\n const dz = Math.abs(position.z - entityPos.z);\n\n if (dx < halfWidth + radius &&\n dy < halfHeight + this.playerHeight / 2 &&\n dz < halfDepth + radius) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Check ground below player\n */\n private checkGround(position: pc.Vec3): number | null {\n // Check against floor entity if exists\n if (this.floorEntity) {\n const floorPos = this.floorEntity.getPosition();\n const floorScale = this.floorEntity.getLocalScale();\n\n // Check if we're above the floor plane\n const halfWidth = floorScale.x / 2 * 100; // Floors are typically large\n const halfDepth = floorScale.z / 2 * 100;\n\n if (Math.abs(position.x - floorPos.x) < halfWidth &&\n Math.abs(position.z - floorPos.z) < halfDepth) {\n return floorPos.y;\n }\n }\n\n // Check against other collision meshes for ground\n let highestGround: number | null = null;\n\n for (const entity of this.collisionEntities) {\n const entityPos = entity.getPosition();\n const entityScale = entity.getLocalScale();\n const meshType = (entity as any)._collisionMeshType;\n\n // Skip floor/plane (handled above) and sphere (not good for walking on)\n if (meshType === 'floor' || meshType === 'plane' || meshType === 'sphere') {\n continue;\n }\n\n let halfWidth: number, halfHeight: number, halfDepth: number;\n\n // Check if entity has custom bounds (for custom meshes)\n const customBounds = (entity as any)._collisionBounds as pc.BoundingBox | undefined;\n if (customBounds) {\n halfWidth = customBounds.halfExtents.x * entityScale.x;\n halfHeight = customBounds.halfExtents.y * entityScale.y;\n halfDepth = customBounds.halfExtents.z * entityScale.z;\n } else {\n halfWidth = entityScale.x / 2;\n halfHeight = entityScale.y / 2;\n halfDepth = entityScale.z / 2;\n }\n\n const topY = entityPos.y + halfHeight;\n\n // Check if we're above this mesh\n if (Math.abs(position.x - entityPos.x) < halfWidth + this.collisionRadius &&\n Math.abs(position.z - entityPos.z) < halfDepth + this.collisionRadius &&\n position.y >= topY - this.stepHeight) {\n if (highestGround === null || topY > highestGround) {\n highestGround = topY;\n }\n }\n }\n\n return highestGround;\n }\n\n /**\n * Update the character controller - call this every frame\n */\n update(dt: number): void {\n if (!this.enabled) return;\n\n // Get movement input\n const moveX = (this.keys['KeyD'] || this.keys['ArrowRight'] ? 1 : 0) -\n (this.keys['KeyA'] || this.keys['ArrowLeft'] ? 1 : 0);\n const moveZ = (this.keys['KeyW'] || this.keys['ArrowUp'] ? 1 : 0) -\n (this.keys['KeyS'] || this.keys['ArrowDown'] ? 1 : 0);\n const isSprinting = this.keys['ShiftLeft'] || this.keys['ShiftRight'];\n\n // Calculate forward and right vectors (horizontal only)\n const yawRad = this.yaw * (Math.PI / 180);\n this.forward.set(-Math.sin(yawRad), 0, -Math.cos(yawRad));\n this.right.set(Math.cos(yawRad), 0, -Math.sin(yawRad));\n\n // Calculate target velocity based on input\n const speed = this.moveSpeed * (isSprinting ? this.sprintMultiplier : 1);\n this.targetVelocity.set(0, 0, 0);\n this.targetVelocity.add(this.tmpVec2.copy(this.forward).mulScalar(moveZ * speed));\n this.targetVelocity.add(this.tmpVec2.copy(this.right).mulScalar(moveX * speed));\n\n // Smooth the horizontal velocity toward target (creates ease-in/ease-out)\n const lerpFactor = damp(this.moveDamping, dt);\n this.horizontalVelocity.lerp(this.horizontalVelocity, this.targetVelocity, lerpFactor);\n\n // Apply gravity (vertical velocity is not smoothed for natural feel)\n if (!this.isGrounded) {\n this.velocity.y -= this.gravity * dt;\n this.velocity.y = Math.max(-this.maxFallSpeed, this.velocity.y);\n }\n\n // Get current position\n const currentPos = this.camera.getPosition().clone();\n const feetY = currentPos.y - this.playerHeight;\n\n // Calculate new position using smoothed horizontal velocity\n const newPos = new pc.Vec3();\n newPos.x = currentPos.x + this.horizontalVelocity.x * dt;\n newPos.y = currentPos.y + this.velocity.y * dt;\n newPos.z = currentPos.z + this.horizontalVelocity.z * dt;\n\n // Check horizontal collision (X axis)\n this.tmpVec2.set(newPos.x, currentPos.y, currentPos.z);\n if (this.checkCollision(this.tmpVec2, this.collisionRadius)) {\n newPos.x = currentPos.x;\n }\n\n // Check horizontal collision (Z axis)\n this.tmpVec2.set(newPos.x, currentPos.y, newPos.z);\n if (this.checkCollision(this.tmpVec2, this.collisionRadius)) {\n newPos.z = currentPos.z;\n }\n\n // Check ground\n const groundY = this.checkGround(newPos);\n const targetFeetY = newPos.y - this.playerHeight;\n\n if (groundY !== null && targetFeetY <= groundY + this.groundCheckDistance) {\n // On ground\n newPos.y = groundY + this.playerHeight;\n this.velocity.y = 0;\n this.isGrounded = true;\n } else if (groundY === null || targetFeetY > groundY + this.stepHeight) {\n // In air\n this.isGrounded = false;\n }\n\n // Apply new position\n this.camera.setPosition(newPos.x, newPos.y, newPos.z);\n\n // Apply rotation\n this.camera.setEulerAngles(this.pitch, this.yaw, 0);\n }\n\n /**\n * Clean up resources\n */\n destroy(): void {\n this.disable();\n\n // Remove collision entities\n for (const entity of this.collisionEntities) {\n entity.destroy();\n }\n this.collisionEntities = [];\n this.floorEntity = null;\n }\n\n /**\n * Get whether the controller is grounded\n */\n get grounded(): boolean {\n return this.isGrounded;\n }\n\n /**\n * Get current velocity\n */\n getVelocity(): pc.Vec3 {\n return this.velocity.clone();\n }\n\n /**\n * Set position\n */\n setPosition(x: number, y: number, z: number): void {\n this.camera.setPosition(x, y, z);\n }\n\n /**\n * Set rotation (yaw and pitch in degrees)\n */\n setRotation(pitch: number, yaw: number): void {\n this.pitch = pitch;\n this.yaw = yaw;\n this.camera.setEulerAngles(pitch, yaw, 0);\n }\n}\n","/**\n * GsplatRevealRadial - Radial reveal effect for gaussian splats\n *\n * Creates two waves emanating from a center point:\n * 1. Dot wave: Small colored dots appear progressively\n * 2. Lift wave: Particles lift up, get highlighted, then settle to original state\n *\n * Ported from PlayCanvas engine examples\n */\n\nimport * as pc from 'playcanvas';\n\nconst shaderGLSL = /* glsl */ `\nuniform float uTime;\nuniform vec3 uCenter;\nuniform float uSpeed;\nuniform float uAcceleration;\nuniform float uDelay;\nuniform vec3 uDotTint;\nuniform vec3 uWaveTint;\nuniform float uOscillationIntensity;\nuniform float uEndRadius;\n\n// Shared globals (initialized once per vertex)\nfloat g_dist;\nfloat g_dotWavePos;\nfloat g_liftTime;\nfloat g_liftWavePos;\n\nvoid initShared(vec3 center) {\n g_dist = length(center - uCenter);\n g_dotWavePos = uSpeed * uTime + 0.5 * uAcceleration * uTime * uTime;\n g_liftTime = max(0.0, uTime - uDelay);\n g_liftWavePos = uSpeed * g_liftTime + 0.5 * uAcceleration * g_liftTime * g_liftTime;\n}\n\n// Hash function for per-splat randomization\nfloat hash(vec3 p) {\n return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);\n}\n\nvoid modifyCenter(inout vec3 center) {\n initShared(center);\n\n // Early exit optimization\n if (g_dist > uEndRadius) return;\n\n // Only apply oscillation if lift wave hasn't fully passed\n bool wavesActive = g_liftTime <= 0.0 || g_dist > g_liftWavePos - 1.5;\n if (wavesActive) {\n // Apply oscillation with per-splat phase offset\n float phase = hash(center) * 6.28318;\n center.y += sin(uTime * 3.0 + phase) * uOscillationIntensity * 0.25;\n }\n\n // Apply lift effect near the wave edge\n float distToLiftWave = abs(g_dist - g_liftWavePos);\n if (distToLiftWave < 1.0 && g_liftTime > 0.0) {\n // Create a smooth lift curve (peaks at wave edge)\n // Lift is 0.9x the oscillation intensity (30% of original 3x)\n float liftAmount = (1.0 - distToLiftWave) * sin(distToLiftWave * 3.14159);\n center.y += liftAmount * uOscillationIntensity * 0.9;\n }\n}\n\nvoid modifyCovariance(vec3 originalCenter, vec3 modifiedCenter, inout vec3 covA, inout vec3 covB) {\n // Early exit for distant splats - hide them\n if (g_dist > uEndRadius) {\n gsplatMakeRound(covA, covB, 0.0);\n return;\n }\n\n // Determine scale and phase\n float scale;\n bool isLiftWave = g_liftTime > 0.0 && g_liftWavePos > g_dist;\n\n if (isLiftWave) {\n // Lift wave: transition from dots to full size\n scale = (g_liftWavePos >= g_dist + 2.0) ? 1.0 : mix(0.1, 1.0, (g_liftWavePos - g_dist) * 0.5);\n } else if (g_dist > g_dotWavePos + 1.0) {\n // Before dot wave: invisible\n gsplatMakeRound(covA, covB, 0.0);\n return;\n } else if (g_dist > g_dotWavePos - 1.0) {\n // Dot wave front: scale from 0 to 0.1 with 2x peak at center\n float distToWave = abs(g_dist - g_dotWavePos);\n scale = (distToWave < 0.5)\n ? mix(0.1, 0.2, 1.0 - distToWave * 2.0)\n : mix(0.0, 0.1, smoothstep(g_dotWavePos + 1.0, g_dotWavePos - 1.0, g_dist));\n } else {\n // After dot wave, before lift: small dots\n scale = 0.1;\n }\n\n // Apply scale to covariance\n if (scale >= 1.0) {\n // Fully revealed: original shape and size (no-op)\n return;\n } else if (isLiftWave) {\n // Lift wave: lerp from round dots to original shape\n float t = (scale - 0.1) * 1.111111; // normalize [0.1, 1.0] to [0, 1]\n float dotSize = scale * 0.05;\n float originalSize = gsplatExtractSize(covA, covB);\n float finalSize = mix(dotSize, originalSize, t);\n\n // Lerp between round and scaled original\n vec3 origCovA = covA * (scale * scale);\n vec3 origCovB = covB * (scale * scale);\n gsplatMakeRound(covA, covB, finalSize);\n covA = mix(covA, origCovA, t);\n covB = mix(covB, origCovB, t);\n } else {\n // Dot phase: round with absolute size, but don't make small splats larger\n float originalSize = gsplatExtractSize(covA, covB);\n gsplatMakeRound(covA, covB, min(scale * 0.05, originalSize));\n }\n}\n\nvoid modifyColor(vec3 center, inout vec4 color) {\n // Use shared globals\n if (g_dist > uEndRadius) return;\n\n // Lift wave tint takes priority (active during lift)\n if (g_liftTime > 0.0 && g_dist >= g_liftWavePos - 1.5 && g_dist <= g_liftWavePos + 0.5) {\n float distToLift = abs(g_dist - g_liftWavePos);\n float liftIntensity = smoothstep(1.5, 0.0, distToLift);\n color.rgb += uWaveTint * liftIntensity;\n }\n // Dot wave tint (active in dot phase, but not where lift wave is active)\n else if (g_dist <= g_dotWavePos && (g_liftTime <= 0.0 || g_dist > g_liftWavePos + 0.5)) {\n float distToDot = abs(g_dist - g_dotWavePos);\n float dotIntensity = smoothstep(1.0, 0.0, distToDot);\n color.rgb += uDotTint * dotIntensity;\n }\n}\n`;\n\nconst shaderWGSL = /* wgsl */ `\nuniform uTime: f32;\nuniform uCenter: vec3f;\nuniform uSpeed: f32;\nuniform uAcceleration: f32;\nuniform uDelay: f32;\nuniform uDotTint: vec3f;\nuniform uWaveTint: vec3f;\nuniform uOscillationIntensity: f32;\nuniform uEndRadius: f32;\n\n// Shared globals (initialized once per vertex)\nvar<private> g_dist: f32;\nvar<private> g_dotWavePos: f32;\nvar<private> g_liftTime: f32;\nvar<private> g_liftWavePos: f32;\n\nfn initShared(center: vec3f) {\n g_dist = length(center - uniform.uCenter);\n g_dotWavePos = uniform.uSpeed * uniform.uTime + 0.5 * uniform.uAcceleration * uniform.uTime * uniform.uTime;\n g_liftTime = max(0.0, uniform.uTime - uniform.uDelay);\n g_liftWavePos = uniform.uSpeed * g_liftTime + 0.5 * uniform.uAcceleration * g_liftTime * g_liftTime;\n}\n\n// Hash function for per-splat randomization\nfn hash(p: vec3f) -> f32 {\n return fract(sin(dot(p, vec3f(127.1, 311.7, 74.7))) * 43758.5453);\n}\n\nfn modifyCenter(center: ptr<function, vec3f>) {\n initShared(*center);\n\n // Early exit optimization\n if (g_dist > uniform.uEndRadius) {\n return;\n }\n\n // Only apply oscillation if lift wave hasn't fully passed\n let wavesActive = g_liftTime <= 0.0 || g_dist > g_liftWavePos - 1.5;\n if (wavesActive) {\n // Apply oscillation with per-splat phase offset\n let phase = hash(*center) * 6.28318;\n (*center).y += sin(uniform.uTime * 3.0 + phase) * uniform.uOscillationIntensity * 0.25;\n }\n\n // Apply lift effect near the wave edge\n let distToLiftWave = abs(g_dist - g_liftWavePos);\n if (distToLiftWave < 1.0 && g_liftTime > 0.0) {\n // Create a smooth lift curve (peaks at wave edge)\n // Lift is 0.9x the oscillation intensity (30% of original 3x)\n let liftAmount = (1.0 - distToLiftWave) * sin(distToLiftWave * 3.14159);\n (*center).y += liftAmount * uniform.uOscillationIntensity * 0.9;\n }\n}\n\nfn modifyCovariance(originalCenter: vec3f, modifiedCenter: vec3f, covA: ptr<function, vec3f>, covB: ptr<function, vec3f>) {\n // Early exit for distant splats - hide them\n if (g_dist > uniform.uEndRadius) {\n gsplatMakeRound(covA, covB, 0.0);\n return;\n }\n\n // Determine scale and phase\n var scale: f32;\n let isLiftWave = g_liftTime > 0.0 && g_liftWavePos > g_dist;\n\n if (isLiftWave) {\n // Lift wave: transition from dots to full size\n scale = select(mix(0.1, 1.0, (g_liftWavePos - g_dist) * 0.5), 1.0, g_liftWavePos >= g_dist + 2.0);\n } else if (g_dist > g_dotWavePos + 1.0) {\n // Before dot wave: invisible\n gsplatMakeRound(covA, covB, 0.0);\n return;\n } else if (g_dist > g_dotWavePos - 1.0) {\n // Dot wave front: scale from 0 to 0.1 with 2x peak at center\n let distToWave = abs(g_dist - g_dotWavePos);\n scale = select(\n mix(0.0, 0.1, smoothstep(g_dotWavePos + 1.0, g_dotWavePos - 1.0, g_dist)),\n mix(0.1, 0.2, 1.0 - distToWave * 2.0),\n distToWave < 0.5\n );\n } else {\n // After dot wave, before lift: small dots\n scale = 0.1;\n }\n\n // Apply scale to covariance\n if (scale >= 1.0) {\n // Fully revealed: original shape and size (no-op)\n return;\n } else if (isLiftWave) {\n // Lift wave: lerp from round dots to original shape\n let t = (scale - 0.1) * 1.111111; // normalize [0.1, 1.0] to [0, 1]\n let dotSize = scale * 0.05;\n let originalSize = gsplatExtractSize(*covA, *covB);\n let finalSize = mix(dotSize, originalSize, t);\n\n // Lerp between round and scaled original\n let origCovA = *covA * (scale * scale);\n let origCovB = *covB * (scale * scale);\n gsplatMakeRound(covA, covB, finalSize);\n *covA = mix(*covA, origCovA, t);\n *covB = mix(*covB, origCovB, t);\n } else {\n // Dot phase: round with absolute size, but don't make small splats larger\n let originalSize = gsplatExtractSize(*covA, *covB);\n gsplatMakeRound(covA, covB, min(scale * 0.05, originalSize));\n }\n}\n\nfn modifyColor(center: vec3f, color: ptr<function, vec4f>) {\n // Use shared globals\n if (g_dist > uniform.uEndRadius) {\n return;\n }\n\n // Lift wave tint takes priority (active during lift)\n if (g_liftTime > 0.0 && g_dist >= g_liftWavePos - 1.5 && g_dist <= g_liftWavePos + 0.5) {\n let distToLift = abs(g_dist - g_liftWavePos);\n let liftIntensity = smoothstep(1.5, 0.0, distToLift);\n (*color) = vec4f((*color).rgb + uniform.uWaveTint * liftIntensity, (*color).a);\n }\n // Dot wave tint (active in dot phase, but not where lift wave is active)\n else if (g_dist <= g_dotWavePos && (g_liftTime <= 0.0 || g_dist > g_liftWavePos + 0.5)) {\n let distToDot = abs(g_dist - g_dotWavePos);\n let dotIntensity = smoothstep(1.0, 0.0, distToDot);\n (*color) = vec4f((*color).rgb + uniform.uDotTint * dotIntensity, (*color).a);\n }\n}\n`;\n\n// Cache for the script class\nlet _GsplatRevealRadialClass: typeof pc.ScriptType | null = null;\n\n/**\n * Gets or creates the GsplatRevealRadial script class.\n * Must be called after PlayCanvas app is initialized.\n */\nexport function getGsplatRevealRadialClass(): typeof pc.ScriptType {\n console.log('[RevealEffect] getGsplatRevealRadialClass called, cached:', !!_GsplatRevealRadialClass);\n\n if (_GsplatRevealRadialClass) {\n return _GsplatRevealRadialClass;\n }\n\n console.log('[RevealEffect] Creating new script class via pc.createScript');\n const GsplatRevealRadial = pc.createScript('gsplatRevealRadial') as any;\n console.log('[RevealEffect] Script class created:', GsplatRevealRadial);\n\n Object.assign(GsplatRevealRadial.prototype, {\n // Properties\n effectTime: 0,\n _materialsApplied: null as Set<pc.Material> | null,\n _shadersApplied: false,\n _retryCount: 0,\n _maxRetries: 100,\n _materialCreatedHandler: null as ((material: pc.Material) => void) | null,\n _systemMaterialHandler: null as ((material: pc.Material, camera: any, layer: any) => void) | null,\n\n // Reusable arrays for uniform updates\n _centerArray: [0, 0, 0],\n _dotTintArray: [0, 0, 0],\n _waveTintArray: [0, 0, 0],\n\n // Effect properties\n center: null as pc.Vec3 | null,\n speed: 5,\n acceleration: 0,\n delay: 2,\n dotTint: null as pc.Color | null,\n waveTint: null as pc.Color | null,\n oscillationIntensity: 0.2,\n endRadius: 25,\n\n initialize(this: any) {\n console.log('[RevealEffect] initialize() called');\n this.effectTime = 0;\n this._materialsApplied = new Set();\n this._shadersApplied = false;\n this._retryCount = 0;\n this._centerArray = [0, 0, 0];\n this._dotTintArray = [0, 0, 0];\n this._waveTintArray = [0, 0, 0];\n\n // Initialize Vec3 and Color if not set\n if (!this.center) {\n this.center = new pc.Vec3(0, 0, 0);\n }\n if (!this.dotTint) {\n this.dotTint = new pc.Color(0, 1, 1); // Cyan\n }\n if (!this.waveTint) {\n this.waveTint = new pc.Color(1, 0.5, 0); // Orange\n }\n\n this.on('enable', () => {\n console.log('[RevealEffect] enabled event fired');\n this.effectTime = 0;\n this._applyShaders();\n });\n\n this.on('disable', () => {\n console.log('[RevealEffect] disabled event fired');\n this._removeShaders();\n });\n\n if (this.enabled) {\n console.log('[RevealEffect] Starting enabled, applying shaders');\n this._applyShaders();\n } else {\n console.log('[RevealEffect] Starting disabled, waiting for enable');\n }\n },\n\n update(this: any, dt: number) {\n if (!this._shadersApplied && this._retryCount < this._maxRetries) {\n this._retryCount++;\n if (this._retryCount % 20 === 0) {\n console.log(`[RevealEffect] Retry ${this._retryCount}/${this._maxRetries} to apply shaders`);\n }\n this._applyShaders();\n }\n\n this.effectTime += dt;\n\n // Log every second\n if (Math.floor(this.effectTime) !== Math.floor(this.effectTime - dt)) {\n console.log(`[RevealEffect] effectTime: ${this.effectTime.toFixed(2)}s, shadersApplied: ${this._shadersApplied}, materialsCount: ${this._materialsApplied?.size || 0}`);\n }\n\n if (this._isEffectComplete()) {\n console.log('[RevealEffect] Effect complete, disabling');\n this.enabled = false;\n return;\n }\n\n this._updateUniforms();\n },\n\n _updateUniforms(this: any) {\n this._setUniform('uTime', this.effectTime);\n\n this._centerArray[0] = this.center.x;\n this._centerArray[1] = this.center.y;\n this._centerArray[2] = this.center.z;\n this._setUniform('uCenter', this._centerArray);\n\n this._setUniform('uSpeed', this.speed);\n this._setUniform('uAcceleration', this.acceleration);\n this._setUniform('uDelay', this.delay);\n\n this._dotTintArray[0] = this.dotTint.r;\n this._dotTintArray[1] = this.dotTint.g;\n this._dotTintArray[2] = this.dotTint.b;\n this._setUniform('uDotTint', this._dotTintArray);\n\n this._waveTintArray[0] = this.waveTint.r;\n this._waveTintArray[1] = this.waveTint.g;\n this._waveTintArray[2] = this.waveTint.b;\n this._setUniform('uWaveTint', this._waveTintArray);\n\n this._setUniform('uOscillationIntensity', this.oscillationIntensity);\n this._setUniform('uEndRadius', this.endRadius);\n },\n\n _getCompletionTime(this: any): number {\n const liftStartTime = this.delay;\n\n if (this.acceleration === 0) {\n return liftStartTime + (this.endRadius / this.speed);\n }\n\n const discriminant = this.speed * this.speed + 2 * this.acceleration * this.endRadius;\n if (discriminant < 0) {\n return Infinity;\n }\n const t = (-this.speed + Math.sqrt(discriminant)) / this.acceleration;\n return liftStartTime + t;\n },\n\n _isEffectComplete(this: any): boolean {\n return this.effectTime >= this._getCompletionTime();\n },\n\n getShaderGLSL(): string {\n return shaderGLSL;\n },\n\n getShaderWGSL(): string {\n return shaderWGSL;\n },\n\n // Shader application methods\n _applyShaders(this: any) {\n const gsplatComponent = this.entity.gsplat;\n if (!gsplatComponent) {\n console.log('[RevealEffect] _applyShaders: No gsplat component found on entity');\n return;\n }\n\n // Check if unified mode is enabled\n const isUnified = (gsplatComponent as any).unified === true;\n const app = this.app;\n\n if (isUnified) {\n console.log('[RevealEffect] Unified mode detected, using GSplatComponentSystem');\n\n // In unified mode, materials are accessed via GSplatComponentSystem\n const gsplatSystem = app?.systems?.gsplat;\n if (gsplatSystem) {\n // Subscribe to material:created event if not already subscribed\n if (!this._systemMaterialHandler) {\n this._systemMaterialHandler = (material: pc.Material, camera: any, layer: any) => {\n console.log('[RevealEffect] material:created event from GSplatComponentSystem');\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n };\n gsplatSystem.on('material:created', this._systemMaterialHandler);\n console.log('[RevealEffect] Subscribed to GSplatComponentSystem material:created event');\n }\n\n // Try to get existing materials for all camera/layer combinations\n const cameras = app.root?.findComponents('camera') || [];\n const layers = gsplatComponent.layers || [0]; // Default to layer 0 if not specified\n\n for (const cameraComp of cameras) {\n for (const layerId of layers) {\n const layer = app.scene?.layers?.getLayerById(layerId);\n if (layer) {\n const material = gsplatSystem.getGSplatMaterial?.(cameraComp.camera, layer);\n if (material) {\n console.log('[RevealEffect] Found unified material via getGSplatMaterial:', material);\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n }\n }\n }\n }\n }\n return;\n }\n\n // Non-unified mode: Check both instance and _instance\n const instance = (gsplatComponent as any).instance || (gsplatComponent as any)._instance;\n\n if (instance) {\n console.log('[RevealEffect] Instance found via component:', instance);\n this._applyToInstance(instance);\n return;\n }\n\n // Try to find gsplat materials through the scene's mesh instances\n if (app && app.scene) {\n // Search through layers for gsplat mesh instances\n const layers = app.scene.layers?.layerList || [];\n for (const layer of layers) {\n if (!layer.meshInstances) continue;\n\n for (const mi of layer.meshInstances) {\n // Check if this is a gsplat mesh instance\n if (mi.gsplatInstance || mi._gsplatInstance) {\n const gsplatInst = mi.gsplatInstance || mi._gsplatInstance;\n console.log('[RevealEffect] Found gsplat instance via mesh instance:', gsplatInst);\n this._applyToInstance(gsplatInst);\n return;\n }\n\n // Check material for gsplat characteristics\n const material = mi.material;\n if (material && (material as any).gsplat) {\n console.log('[RevealEffect] Found gsplat material via mesh instance:', material);\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n return;\n }\n }\n }\n\n // Also check root entities for gsplat components\n const checkEntity = (entity: any) => {\n if (entity.gsplat) {\n const inst = entity.gsplat.instance || entity.gsplat._instance;\n if (inst) {\n console.log('[RevealEffect] Found gsplat instance via entity search:', inst);\n this._applyToInstance(inst);\n return true;\n }\n }\n for (const child of entity.children || []) {\n if (checkEntity(child)) return true;\n }\n return false;\n };\n\n if (app.root) {\n checkEntity(app.root);\n }\n }\n\n // Log state for debugging\n if (this._retryCount % 50 === 0) {\n console.log('[RevealEffect] Still searching for gsplat materials...');\n console.log('[RevealEffect] gsplatComponent.unified:', (gsplatComponent as any).unified);\n console.log('[RevealEffect] gsplatComponent.asset:', gsplatComponent.asset);\n }\n },\n\n _applyToInstance(this: any, instance: any) {\n if (this._shadersApplied) {\n return;\n }\n\n console.log('[RevealEffect] Applying shaders to instance');\n console.log('[RevealEffect] Instance type:', instance.constructor?.name);\n console.log('[RevealEffect] Instance keys:', Object.keys(instance));\n\n // Try to find materials on the instance\n const materials = instance.materials || instance._materials;\n console.log('[RevealEffect] Instance materials:', materials);\n\n if (materials) {\n if (materials instanceof Map || (materials.forEach && materials.size !== undefined)) {\n console.log('[RevealEffect] Materials is a Map/Set with size:', materials.size);\n if (materials.size > 0) {\n materials.forEach((material: pc.Material) => {\n this._applyShaderToMaterial(material);\n });\n this._shadersApplied = true;\n console.log('[RevealEffect] SUCCESS: Shaders applied to', materials.size, 'materials');\n }\n } else if (Array.isArray(materials)) {\n console.log('[RevealEffect] Materials is array with length:', materials.length);\n materials.forEach((material: pc.Material) => {\n this._applyShaderToMaterial(material);\n });\n this._shadersApplied = true;\n }\n }\n\n // Also try material property directly\n if (!this._shadersApplied) {\n const material = instance.material || instance._material;\n if (material) {\n console.log('[RevealEffect] Found single material on instance');\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n }\n }\n\n // Subscribe to material:created for future materials\n if (instance.on && !this._materialCreatedHandler) {\n this._materialCreatedHandler = (material: pc.Material) => {\n console.log('[RevealEffect] material:created event received');\n this._applyShaderToMaterial(material);\n this._shadersApplied = true;\n };\n instance.on('material:created', this._materialCreatedHandler);\n console.log('[RevealEffect] Subscribed to material:created event');\n }\n },\n\n _applyShaderToMaterial(this: any, material: pc.Material) {\n if (this._materialsApplied.has(material)) {\n console.log('[RevealEffect] Material already has shader applied, skipping');\n return;\n }\n\n console.log('[RevealEffect] Applying shader to material:', material);\n console.log('[RevealEffect] Material constructor:', material.constructor?.name);\n console.log('[RevealEffect] Material keys:', Object.keys(material));\n\n // Log all methods on the material\n const methods: string[] = [];\n let obj = material;\n while (obj && obj !== Object.prototype) {\n const names = Object.getOwnPropertyNames(obj);\n for (const name of names) {\n try {\n if (typeof (obj as any)[name] === 'function' && !methods.includes(name)) {\n methods.push(name);\n }\n } catch (e) {}\n }\n obj = Object.getPrototypeOf(obj);\n }\n console.log('[RevealEffect] Material methods:', methods.filter(m => !m.startsWith('_')).join(', '));\n\n const glsl = this.getShaderGLSL();\n const wgsl = this.getShaderWGSL();\n\n // Try setShaderChunk first (newer API)\n const hasSetShaderChunk = typeof (material as any).setShaderChunk === 'function';\n console.log('[RevealEffect] material.setShaderChunk exists:', hasSetShaderChunk);\n\n if (hasSetShaderChunk) {\n if (glsl) {\n (material as any).setShaderChunk('gsplatEffectGLSL', glsl);\n console.log('[RevealEffect] GLSL shader chunk set via setShaderChunk');\n }\n if (wgsl) {\n (material as any).setShaderChunk('gsplatEffectWGSL', wgsl);\n console.log('[RevealEffect] WGSL shader chunk set via setShaderChunk');\n }\n } else {\n // Try alternative: chunks property (older API)\n if ((material as any).chunks) {\n console.log('[RevealEffect] Material has chunks property');\n (material as any).chunks.gsplatEffectGLSL = glsl;\n (material as any).chunks.gsplatEffectWGSL = wgsl;\n console.log('[RevealEffect] Set chunks directly');\n }\n\n // Try: customFragmentShader or options\n if ((material as any).options) {\n console.log('[RevealEffect] Material has options:', (material as any).options);\n }\n\n // Try: shader property\n if ((material as any).shader) {\n console.log('[RevealEffect] Material has shader:', (material as any).shader);\n }\n\n // Try: setParameter for uniforms (this should work)\n if (typeof material.setParameter === 'function') {\n console.log('[RevealEffect] material.setParameter exists - will use for uniforms');\n }\n }\n\n material.update?.();\n this._materialsApplied.add(material);\n console.log('[RevealEffect] Material added to applied set, total:', this._materialsApplied.size);\n },\n\n _removeShaders(this: any) {\n if (this._materialsApplied) {\n this._materialsApplied.forEach((material: pc.Material) => {\n (material as any).setShaderChunk?.('gsplatEffectGLSL', '');\n (material as any).setShaderChunk?.('gsplatEffectWGSL', '');\n material.update?.();\n });\n this._materialsApplied.clear();\n }\n this._shadersApplied = false;\n\n // Clean up instance-level handler (non-unified mode)\n if (this._materialCreatedHandler) {\n const gsplatComponent = this.entity.gsplat;\n const gsplatInstance = (gsplatComponent as any)?.instance;\n if (gsplatInstance?.off) {\n gsplatInstance.off('material:created', this._materialCreatedHandler);\n }\n this._materialCreatedHandler = null;\n }\n\n // Clean up system-level handler (unified mode)\n if (this._systemMaterialHandler) {\n const gsplatSystem = this.app?.systems?.gsplat;\n if (gsplatSystem?.off) {\n gsplatSystem.off('material:created', this._systemMaterialHandler);\n }\n this._systemMaterialHandler = null;\n }\n },\n\n _setUniform(this: any, name: string, value: number | number[]) {\n if (this._materialsApplied) {\n this._materialsApplied.forEach((material: pc.Material) => {\n material.setParameter?.(name, value);\n });\n }\n },\n\n destroy(this: any) {\n this._removeShaders();\n }\n });\n\n _GsplatRevealRadialClass = GsplatRevealRadial;\n return GsplatRevealRadial;\n}\n\n// For backwards compatibility\nexport const GsplatRevealRadial = {\n get class() {\n return getGsplatRevealRadialClass();\n }\n};\n","/**\n * Reveal Effect Presets\n *\n * Pre-configured settings for the radial reveal effect.\n * Users can choose from fast, medium, or slow presets.\n */\n\nexport interface RevealPresetConfig {\n /** Base wave speed in units/second */\n speed: number;\n /** Speed increase over time */\n acceleration: number;\n /** Time offset before lift wave starts (seconds) */\n delay: number;\n /** Position oscillation strength */\n oscillationIntensity: number;\n /** Additive color for initial dots */\n dotTint: { r: number; g: number; b: number };\n /** Additive color for lift wave highlight */\n waveTint: { r: number; g: number; b: number };\n /** Distance at which to disable effect for performance */\n endRadius: number;\n}\n\n/**\n * Reveal effect presets\n */\nexport const REVEAL_PRESETS: Record<string, RevealPresetConfig> = {\n /**\n * Fast preset - Quick reveal for shorter loading experiences\n * Duration: ~3-4 seconds for a 50 unit radius scene\n */\n fast: {\n speed: 10,\n acceleration: 2,\n delay: 0.5,\n oscillationIntensity: 0.1,\n dotTint: { r: 0, g: 1, b: 1 }, // Cyan\n waveTint: { r: 1, g: 0.5, b: 0 }, // Orange\n endRadius: 50\n },\n\n /**\n * Medium preset - Balanced reveal for typical scenes\n * Duration: ~5-7 seconds for a 50 unit radius scene\n */\n medium: {\n speed: 5,\n acceleration: 0,\n delay: 2,\n oscillationIntensity: 0.2,\n dotTint: { r: 0, g: 1, b: 1 }, // Cyan\n waveTint: { r: 1, g: 0.5, b: 0 }, // Orange\n endRadius: 50\n },\n\n /**\n * Slow preset - Dramatic reveal for showcase/demo scenes\n * Duration: ~12-15 seconds for a 50 unit radius scene\n */\n slow: {\n speed: 3,\n acceleration: 0,\n delay: 3,\n oscillationIntensity: 0.25,\n dotTint: { r: 0, g: 1, b: 1 }, // Cyan\n waveTint: { r: 1, g: 0.5, b: 0 }, // Orange\n endRadius: 50\n }\n};\n\n/**\n * Type for reveal effect preset names\n */\nexport type RevealPreset = 'fast' | 'medium' | 'slow' | 'none';\n\n/**\n * Get preset configuration by name\n * Returns undefined for 'none' or invalid presets\n */\nexport function getRevealPreset(preset: RevealPreset): RevealPresetConfig | undefined {\n if (preset === 'none') {\n return undefined;\n }\n return REVEAL_PRESETS[preset];\n}\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.loop = exports.conditional = exports.parse = void 0;\n\nvar parse = function parse(stream, schema) {\n var result = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n var parent = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : result;\n\n if (Array.isArray(schema)) {\n schema.forEach(function (partSchema) {\n return parse(stream, partSchema, result, parent);\n });\n } else if (typeof schema === 'function') {\n schema(stream, result, parent, parse);\n } else {\n var key = Object.keys(schema)[0];\n\n if (Array.isArray(schema[key])) {\n parent[key] = {};\n parse(stream, schema[key], result, parent[key]);\n } else {\n parent[key] = schema[key](stream, result, parent, parse);\n }\n }\n\n return result;\n};\n\nexports.parse = parse;\n\nvar conditional = function conditional(schema, conditionFunc) {\n return function (stream, result, parent, parse) {\n if (conditionFunc(stream, result, parent)) {\n parse(stream, schema, result, parent);\n }\n };\n};\n\nexports.conditional = conditional;\n\nvar loop = function loop(schema, continueFunc) {\n return function (stream, result, parent, parse) {\n var arr = [];\n var lastStreamPos = stream.pos;\n\n while (continueFunc(stream, result, parent)) {\n var newParent = {};\n parse(stream, schema, result, newParent); // cases when whole file is parsed but no termination is there and stream position is not getting updated as well\n // it falls into infinite recursion, null check to avoid the same\n\n if (stream.pos === lastStreamPos) {\n break;\n }\n\n lastStreamPos = stream.pos;\n arr.push(newParent);\n }\n\n return arr;\n };\n};\n\nexports.loop = loop;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.readBits = exports.readArray = exports.readUnsigned = exports.readString = exports.peekBytes = exports.readBytes = exports.peekByte = exports.readByte = exports.buildStream = void 0;\n\n// Default stream and parsers for Uint8TypedArray data type\nvar buildStream = function buildStream(uint8Data) {\n return {\n data: uint8Data,\n pos: 0\n };\n};\n\nexports.buildStream = buildStream;\n\nvar readByte = function readByte() {\n return function (stream) {\n return stream.data[stream.pos++];\n };\n};\n\nexports.readByte = readByte;\n\nvar peekByte = function peekByte() {\n var offset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;\n return function (stream) {\n return stream.data[stream.pos + offset];\n };\n};\n\nexports.peekByte = peekByte;\n\nvar readBytes = function readBytes(length) {\n return function (stream) {\n return stream.data.subarray(stream.pos, stream.pos += length);\n };\n};\n\nexports.readBytes = readBytes;\n\nvar peekBytes = function peekBytes(length) {\n return function (stream) {\n return stream.data.subarray(stream.pos, stream.pos + length);\n };\n};\n\nexports.peekBytes = peekBytes;\n\nvar readString = function readString(length) {\n return function (stream) {\n return Array.from(readBytes(length)(stream)).map(function (value) {\n return String.fromCharCode(value);\n }).join('');\n };\n};\n\nexports.readString = readString;\n\nvar readUnsigned = function readUnsigned(littleEndian) {\n return function (stream) {\n var bytes = readBytes(2)(stream);\n return littleEndian ? (bytes[1] << 8) + bytes[0] : (bytes[0] << 8) + bytes[1];\n };\n};\n\nexports.readUnsigned = readUnsigned;\n\nvar readArray = function readArray(byteSize, totalOrFunc) {\n return function (stream, result, parent) {\n var total = typeof totalOrFunc === 'function' ? totalOrFunc(stream, result, parent) : totalOrFunc;\n var parser = readBytes(byteSize);\n var arr = new Array(total);\n\n for (var i = 0; i < total; i++) {\n arr[i] = parser(stream);\n }\n\n return arr;\n };\n};\n\nexports.readArray = readArray;\n\nvar subBitsTotal = function subBitsTotal(bits, startIndex, length) {\n var result = 0;\n\n for (var i = 0; i < length; i++) {\n result += bits[startIndex + i] && Math.pow(2, length - i - 1);\n }\n\n return result;\n};\n\nvar readBits = function readBits(schema) {\n return function (stream) {\n var _byte = readByte()(stream); // convert the byte to bit array\n\n\n var bits = new Array(8);\n\n for (var i = 0; i < 8; i++) {\n bits[7 - i] = !!(_byte & 1 << i);\n } // convert the bit array to values based on the schema\n\n\n return Object.keys(schema).reduce(function (res, key) {\n var def = schema[key];\n\n if (def.length) {\n res[key] = subBitsTotal(bits, def.index, def.length);\n } else {\n res[key] = bits[def.index];\n }\n\n return res;\n }, {});\n };\n};\n\nexports.readBits = readBits;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.decompressFrames = exports.decompressFrame = exports.parseGIF = void 0;\n\nvar _gif = _interopRequireDefault(require(\"js-binary-schema-parser/lib/schemas/gif\"));\n\nvar _jsBinarySchemaParser = require(\"js-binary-schema-parser\");\n\nvar _uint = require(\"js-binary-schema-parser/lib/parsers/uint8\");\n\nvar _deinterlace = require(\"./deinterlace\");\n\nvar _lzw = require(\"./lzw\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar parseGIF = function parseGIF(arrayBuffer) {\n var byteData = new Uint8Array(arrayBuffer);\n return (0, _jsBinarySchemaParser.parse)((0, _uint.buildStream)(byteData), _gif[\"default\"]);\n};\n\nexports.parseGIF = parseGIF;\n\nvar generatePatch = function generatePatch(image) {\n var totalPixels = image.pixels.length;\n var patchData = new Uint8ClampedArray(totalPixels * 4);\n\n for (var i = 0; i < totalPixels; i++) {\n var pos = i * 4;\n var colorIndex = image.pixels[i];\n var color = image.colorTable[colorIndex] || [0, 0, 0];\n patchData[pos] = color[0];\n patchData[pos + 1] = color[1];\n patchData[pos + 2] = color[2];\n patchData[pos + 3] = colorIndex !== image.transparentIndex ? 255 : 0;\n }\n\n return patchData;\n};\n\nvar decompressFrame = function decompressFrame(frame, gct, buildImagePatch) {\n if (!frame.image) {\n console.warn('gif frame does not have associated image.');\n return;\n }\n\n var image = frame.image; // get the number of pixels\n\n var totalPixels = image.descriptor.width * image.descriptor.height; // do lzw decompression\n\n var pixels = (0, _lzw.lzw)(image.data.minCodeSize, image.data.blocks, totalPixels); // deal with interlacing if necessary\n\n if (image.descriptor.lct.interlaced) {\n pixels = (0, _deinterlace.deinterlace)(pixels, image.descriptor.width);\n }\n\n var resultImage = {\n pixels: pixels,\n dims: {\n top: frame.image.descriptor.top,\n left: frame.image.descriptor.left,\n width: frame.image.descriptor.width,\n height: frame.image.descriptor.height\n }\n }; // color table\n\n if (image.descriptor.lct && image.descriptor.lct.exists) {\n resultImage.colorTable = image.lct;\n } else {\n resultImage.colorTable = gct;\n } // add per frame relevant gce information\n\n\n if (frame.gce) {\n resultImage.delay = (frame.gce.delay || 10) * 10; // convert to ms\n\n resultImage.disposalType = frame.gce.extras.disposal; // transparency\n\n if (frame.gce.extras.transparentColorGiven) {\n resultImage.transparentIndex = frame.gce.transparentColorIndex;\n }\n } // create canvas usable imagedata if desired\n\n\n if (buildImagePatch) {\n resultImage.patch = generatePatch(resultImage);\n }\n\n return resultImage;\n};\n\nexports.decompressFrame = decompressFrame;\n\nvar decompressFrames = function decompressFrames(parsedGif, buildImagePatches) {\n return parsedGif.frames.filter(function (f) {\n return f.image;\n }).map(function (f) {\n return decompressFrame(f, parsedGif.gct, buildImagePatches);\n });\n};\n\nexports.decompressFrames = decompressFrames;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _ = require(\"../\");\n\nvar _uint = require(\"../parsers/uint8\");\n\n// a set of 0x00 terminated subblocks\nvar subBlocksSchema = {\n blocks: function blocks(stream) {\n var terminator = 0x00;\n var chunks = [];\n var streamSize = stream.data.length;\n var total = 0;\n\n for (var size = (0, _uint.readByte)()(stream); size !== terminator; size = (0, _uint.readByte)()(stream)) {\n // size becomes undefined for some case when file is corrupted and terminator is not proper \n // null check to avoid recursion\n if (!size) break; // catch corrupted files with no terminator\n\n if (stream.pos + size >= streamSize) {\n var availableSize = streamSize - stream.pos;\n chunks.push((0, _uint.readBytes)(availableSize)(stream));\n total += availableSize;\n break;\n }\n\n chunks.push((0, _uint.readBytes)(size)(stream));\n total += size;\n }\n\n var result = new Uint8Array(total);\n var offset = 0;\n\n for (var i = 0; i < chunks.length; i++) {\n result.set(chunks[i], offset);\n offset += chunks[i].length;\n }\n\n return result;\n }\n}; // global control extension\n\nvar gceSchema = (0, _.conditional)({\n gce: [{\n codes: (0, _uint.readBytes)(2)\n }, {\n byteSize: (0, _uint.readByte)()\n }, {\n extras: (0, _uint.readBits)({\n future: {\n index: 0,\n length: 3\n },\n disposal: {\n index: 3,\n length: 3\n },\n userInput: {\n index: 6\n },\n transparentColorGiven: {\n index: 7\n }\n })\n }, {\n delay: (0, _uint.readUnsigned)(true)\n }, {\n transparentColorIndex: (0, _uint.readByte)()\n }, {\n terminator: (0, _uint.readByte)()\n }]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0xf9;\n}); // image pipeline block\n\nvar imageSchema = (0, _.conditional)({\n image: [{\n code: (0, _uint.readByte)()\n }, {\n descriptor: [{\n left: (0, _uint.readUnsigned)(true)\n }, {\n top: (0, _uint.readUnsigned)(true)\n }, {\n width: (0, _uint.readUnsigned)(true)\n }, {\n height: (0, _uint.readUnsigned)(true)\n }, {\n lct: (0, _uint.readBits)({\n exists: {\n index: 0\n },\n interlaced: {\n index: 1\n },\n sort: {\n index: 2\n },\n future: {\n index: 3,\n length: 2\n },\n size: {\n index: 5,\n length: 3\n }\n })\n }]\n }, (0, _.conditional)({\n lct: (0, _uint.readArray)(3, function (stream, result, parent) {\n return Math.pow(2, parent.descriptor.lct.size + 1);\n })\n }, function (stream, result, parent) {\n return parent.descriptor.lct.exists;\n }), {\n data: [{\n minCodeSize: (0, _uint.readByte)()\n }, subBlocksSchema]\n }]\n}, function (stream) {\n return (0, _uint.peekByte)()(stream) === 0x2c;\n}); // plain text block\n\nvar textSchema = (0, _.conditional)({\n text: [{\n codes: (0, _uint.readBytes)(2)\n }, {\n blockSize: (0, _uint.readByte)()\n }, {\n preData: function preData(stream, result, parent) {\n return (0, _uint.readBytes)(parent.text.blockSize)(stream);\n }\n }, subBlocksSchema]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0x01;\n}); // application block\n\nvar applicationSchema = (0, _.conditional)({\n application: [{\n codes: (0, _uint.readBytes)(2)\n }, {\n blockSize: (0, _uint.readByte)()\n }, {\n id: function id(stream, result, parent) {\n return (0, _uint.readString)(parent.blockSize)(stream);\n }\n }, subBlocksSchema]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0xff;\n}); // comment block\n\nvar commentSchema = (0, _.conditional)({\n comment: [{\n codes: (0, _uint.readBytes)(2)\n }, subBlocksSchema]\n}, function (stream) {\n var codes = (0, _uint.peekBytes)(2)(stream);\n return codes[0] === 0x21 && codes[1] === 0xfe;\n});\nvar schema = [{\n header: [{\n signature: (0, _uint.readString)(3)\n }, {\n version: (0, _uint.readString)(3)\n }]\n}, {\n lsd: [{\n width: (0, _uint.readUnsigned)(true)\n }, {\n height: (0, _uint.readUnsigned)(true)\n }, {\n gct: (0, _uint.readBits)({\n exists: {\n index: 0\n },\n resolution: {\n index: 1,\n length: 3\n },\n sort: {\n index: 4\n },\n size: {\n index: 5,\n length: 3\n }\n })\n }, {\n backgroundColorIndex: (0, _uint.readByte)()\n }, {\n pixelAspectRatio: (0, _uint.readByte)()\n }]\n}, (0, _.conditional)({\n gct: (0, _uint.readArray)(3, function (stream, result) {\n return Math.pow(2, result.lsd.gct.size + 1);\n })\n}, function (stream, result) {\n return result.lsd.gct.exists;\n}), // content frames\n{\n frames: (0, _.loop)([gceSchema, applicationSchema, commentSchema, imageSchema, textSchema], function (stream) {\n var nextCode = (0, _uint.peekByte)()(stream); // rather than check for a terminator, we should check for the existence\n // of an ext or image block to avoid infinite loops\n //var terminator = 0x3B;\n //return nextCode !== terminator;\n\n return nextCode === 0x21 || nextCode === 0x2c;\n })\n}];\nvar _default = schema;\nexports[\"default\"] = _default;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.deinterlace = void 0;\n\n/**\r\n * Deinterlace function from https://github.com/shachaf/jsgif\r\n */\nvar deinterlace = function deinterlace(pixels, width) {\n var newPixels = new Array(pixels.length);\n var rows = pixels.length / width;\n\n var cpRow = function cpRow(toRow, fromRow) {\n var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);\n newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));\n }; // See appendix E.\n\n\n var offsets = [0, 4, 2, 1];\n var steps = [8, 8, 4, 2];\n var fromRow = 0;\n\n for (var pass = 0; pass < 4; pass++) {\n for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {\n cpRow(toRow, fromRow);\n fromRow++;\n }\n }\n\n return newPixels;\n};\n\nexports.deinterlace = deinterlace;","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.lzw = void 0;\n\n/**\r\n * javascript port of java LZW decompression\r\n * Original java author url: https://gist.github.com/devunwired/4479231\r\n */\nvar lzw = function lzw(minCodeSize, data, pixelCount) {\n var MAX_STACK_SIZE = 4096;\n var nullCode = -1;\n var npix = pixelCount;\n var available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, i, datum, data_size, first, top, bi, pi;\n var dstPixels = new Array(pixelCount);\n var prefix = new Array(MAX_STACK_SIZE);\n var suffix = new Array(MAX_STACK_SIZE);\n var pixelStack = new Array(MAX_STACK_SIZE + 1); // Initialize GIF data stream decoder.\n\n data_size = minCodeSize;\n clear = 1 << data_size;\n end_of_information = clear + 1;\n available = clear + 2;\n old_code = nullCode;\n code_size = data_size + 1;\n code_mask = (1 << code_size) - 1;\n\n for (code = 0; code < clear; code++) {\n prefix[code] = 0;\n suffix[code] = code;\n } // Decode GIF pixel stream.\n\n\n var datum, bits, count, first, top, pi, bi;\n datum = bits = count = first = top = pi = bi = 0;\n\n for (i = 0; i < npix;) {\n if (top === 0) {\n if (bits < code_size) {\n // get the next byte\n datum += data[bi] << bits;\n bits += 8;\n bi++;\n continue;\n } // Get the next code.\n\n\n code = datum & code_mask;\n datum >>= code_size;\n bits -= code_size; // Interpret the code\n\n if (code > available || code == end_of_information) {\n break;\n }\n\n if (code == clear) {\n // Reset decoder.\n code_size = data_size + 1;\n code_mask = (1 << code_size) - 1;\n available = clear + 2;\n old_code = nullCode;\n continue;\n }\n\n if (old_code == nullCode) {\n pixelStack[top++] = suffix[code];\n old_code = code;\n first = code;\n continue;\n }\n\n in_code = code;\n\n if (code == available) {\n pixelStack[top++] = first;\n code = old_code;\n }\n\n while (code > clear) {\n pixelStack[top++] = suffix[code];\n code = prefix[code];\n }\n\n first = suffix[code] & 0xff;\n pixelStack[top++] = first; // add a new string to the table, but only if space is available\n // if not, just continue with current table until a clear code is found\n // (deferred clear code implementation as per GIF spec)\n\n if (available < MAX_STACK_SIZE) {\n prefix[available] = old_code;\n suffix[available] = first;\n available++;\n\n if ((available & code_mask) === 0 && available < MAX_STACK_SIZE) {\n code_size++;\n code_mask += available;\n }\n }\n\n old_code = in_code;\n } // Pop a pixel off the pixel stack.\n\n\n top--;\n dstPixels[pi++] = pixelStack[top];\n i++;\n }\n\n for (i = pi; i < npix; i++) {\n dstPixels[i] = 0; // clear missing pixels\n }\n\n return dstPixels;\n};\n\nexports.lzw = lzw;","/**\n * Animated GIF Helper for PlayCanvas\n *\n * Uses gifuct-js to parse and decompress GIF frames, then renders them\n * to a PlayCanvas texture with proper transparency support.\n */\n\nimport * as pc from 'playcanvas';\nimport { parseGIF, decompressFrames } from 'gifuct-js';\n\n/**\n * Represents a single GIF frame\n */\ninterface GifFrame {\n dims: {\n width: number;\n height: number;\n top: number;\n left: number;\n };\n patch: Uint8ClampedArray;\n delay: number;\n disposalType?: number;\n}\n\n/**\n * Options for creating an animated GIF texture\n */\nexport interface AnimatedGifOptions {\n /** Auto-play the GIF when loaded */\n autoPlay?: boolean;\n /** Callback when the first frame is ready */\n onReady?: () => void;\n /** Callback on error */\n onError?: (error: Error) => void;\n}\n\n/**\n * Animated GIF texture for PlayCanvas\n * Handles parsing, decompression, and frame-by-frame rendering with transparency\n */\nexport class AnimatedGifTexture {\n private app: pc.Application;\n private url: string;\n private frames: GifFrame[] = [];\n private currentFrameIndex = 0;\n private isPlaying = false;\n private isLoaded = false;\n private lastFrameTime = 0;\n private updateHandler: (() => void) | null = null;\n private options: AnimatedGifOptions;\n\n // Canvas for compositing frames (handles disposal and transparency)\n private canvas: HTMLCanvasElement;\n private ctx: CanvasRenderingContext2D;\n\n // PlayCanvas texture\n public texture: pc.Texture | null = null;\n\n // GIF dimensions\n private gifWidth = 0;\n private gifHeight = 0;\n\n constructor(app: pc.Application, url: string, options: AnimatedGifOptions = {}) {\n this.app = app;\n this.url = url;\n this.options = options;\n\n // Create offscreen canvas for compositing\n this.canvas = document.createElement('canvas');\n this.ctx = this.canvas.getContext('2d', { willReadFrequently: true })!;\n\n // Start loading\n this.load();\n }\n\n /**\n * Load and parse the GIF\n */\n private async load(): Promise<void> {\n try {\n // Fetch GIF data\n const response = await fetch(this.url);\n if (!response.ok) {\n throw new Error(`Failed to fetch GIF: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n\n // Parse GIF\n const gif = parseGIF(buffer);\n this.frames = decompressFrames(gif, true) as unknown as GifFrame[];\n\n if (this.frames.length === 0) {\n throw new Error('GIF has no frames');\n }\n\n // Get dimensions from first frame\n this.gifWidth = gif.lsd.width;\n this.gifHeight = gif.lsd.height;\n\n // Setup canvas\n this.canvas.width = this.gifWidth;\n this.canvas.height = this.gifHeight;\n\n // Create PlayCanvas texture\n this.texture = new pc.Texture(this.app.graphicsDevice, {\n width: this.gifWidth,\n height: this.gifHeight,\n format: pc.PIXELFORMAT_RGBA8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n\n // Draw first frame\n this.drawFrame(0);\n\n this.isLoaded = true;\n\n console.log(`[AnimatedGif] Loaded GIF: ${this.url}, ${this.frames.length} frames, ${this.gifWidth}x${this.gifHeight}`);\n\n // Notify ready\n if (this.options.onReady) {\n this.options.onReady();\n }\n\n // Auto-play if requested\n if (this.options.autoPlay) {\n this.play();\n }\n } catch (error) {\n console.error('[AnimatedGif] Error loading GIF:', error);\n if (this.options.onError) {\n this.options.onError(error instanceof Error ? error : new Error(String(error)));\n }\n }\n }\n\n /**\n * Draw a specific frame to the canvas and update the texture\n */\n private drawFrame(frameIndex: number): void {\n if (!this.texture || frameIndex >= this.frames.length) return;\n\n const frame = this.frames[frameIndex];\n const prevFrame = frameIndex > 0 ? this.frames[frameIndex - 1] : null;\n\n // Handle disposal of previous frame\n if (prevFrame && prevFrame.disposalType === 2) {\n // Restore to background (clear the previous frame area)\n this.ctx.clearRect(\n prevFrame.dims.left,\n prevFrame.dims.top,\n prevFrame.dims.width,\n prevFrame.dims.height\n );\n }\n // disposalType === 3 (restore to previous) is complex and rarely used, skip for now\n\n // Create ImageData from frame patch\n const imageData = new ImageData(\n new Uint8ClampedArray(frame.patch),\n frame.dims.width,\n frame.dims.height\n );\n\n // Create temporary canvas for this frame\n const tempCanvas = document.createElement('canvas');\n tempCanvas.width = frame.dims.width;\n tempCanvas.height = frame.dims.height;\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(imageData, 0, 0);\n\n // Draw frame patch at correct position\n this.ctx.drawImage(\n tempCanvas,\n frame.dims.left,\n frame.dims.top\n );\n\n // Update texture from canvas\n this.updateTexture();\n }\n\n /**\n * Update the PlayCanvas texture from the canvas\n */\n private updateTexture(): void {\n if (!this.texture) return;\n\n // Get pixel data from canvas\n const imageData = this.ctx.getImageData(0, 0, this.gifWidth, this.gifHeight);\n\n // Upload to texture\n const pixels = this.texture.lock();\n if (pixels) {\n pixels.set(imageData.data);\n }\n this.texture.unlock();\n this.texture.upload();\n }\n\n /**\n * Animation update - called each frame when playing\n */\n private update = (): void => {\n if (!this.isPlaying || !this.isLoaded || this.frames.length <= 1) return;\n\n const now = performance.now();\n const frame = this.frames[this.currentFrameIndex];\n const delay = frame.delay || 100; // Default 100ms if not specified\n\n if (now - this.lastFrameTime >= delay) {\n // Advance to next frame\n this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.length;\n this.drawFrame(this.currentFrameIndex);\n this.lastFrameTime = now;\n }\n };\n\n /**\n * Start playing the GIF animation\n */\n public play(): void {\n if (this.isPlaying) return;\n\n this.isPlaying = true;\n this.lastFrameTime = performance.now();\n\n // Register update handler\n if (!this.updateHandler) {\n this.updateHandler = this.update;\n this.app.on('update', this.updateHandler);\n }\n }\n\n /**\n * Pause the GIF animation\n */\n public pause(): void {\n if (!this.isPlaying) return;\n\n this.isPlaying = false;\n\n // Remove update handler\n if (this.updateHandler) {\n this.app.off('update', this.updateHandler);\n this.updateHandler = null;\n }\n }\n\n /**\n * Stop and reset to first frame\n */\n public stop(): void {\n this.pause();\n this.currentFrameIndex = 0;\n if (this.isLoaded) {\n // Clear canvas and redraw first frame\n this.ctx.clearRect(0, 0, this.gifWidth, this.gifHeight);\n this.drawFrame(0);\n }\n }\n\n /**\n * Check if the GIF is currently playing\n */\n public get playing(): boolean {\n return this.isPlaying;\n }\n\n /**\n * Check if the GIF is loaded\n */\n public get loaded(): boolean {\n return this.isLoaded;\n }\n\n /**\n * Clean up resources\n */\n public destroy(): void {\n this.pause();\n\n if (this.texture) {\n this.texture.destroy();\n this.texture = null;\n }\n\n this.frames = [];\n this.isLoaded = false;\n }\n}\n\n/**\n * Helper to detect if a URL is a GIF\n */\nexport function isGifUrl(url: string): boolean {\n const lower = url.toLowerCase();\n // Check extension or content type hint\n return lower.endsWith('.gif') || lower.includes('image/gif') || lower.includes('format=gif');\n}\n","/**\n * HTML Mesh Helper for PlayCanvas\n *\n * Renders HTML elements as textures on 3D meshes using the texElement2D API\n * (HTML-in-Canvas proposal) with fallback to canvas rendering.\n *\n * Based on PlayCanvas PR #7897: https://github.com/playcanvas/engine/pull/7897\n */\n\nimport * as pc from 'playcanvas';\n\n/**\n * Patch PlayCanvas GraphicsDevice to add _isHTMLElementInterface method\n * This is required for HTML-in-Canvas support (texElement2D API)\n *\n * The postinstall patches only modify ESM source files, but production builds\n * may use the bundled playcanvas.js which doesn't have the patches.\n * This runtime patch ensures the method exists regardless of which build is used.\n */\nfunction patchPlayCanvasForHtmlMesh(): void {\n // Get the GraphicsDevice class from pc\n const GraphicsDevice = (pc as any).GraphicsDevice;\n if (!GraphicsDevice) {\n console.warn('[HtmlMeshHelper] Could not find pc.GraphicsDevice to patch');\n return;\n }\n\n // Check if already patched\n if (typeof GraphicsDevice.prototype._isHTMLElementInterface === 'function') {\n return; // Already patched\n }\n\n // Add the _isHTMLElementInterface method\n GraphicsDevice.prototype._isHTMLElementInterface = function(texture: any): boolean {\n return typeof HTMLElement !== 'undefined' &&\n texture instanceof HTMLElement &&\n !(texture instanceof HTMLImageElement) &&\n !(texture instanceof HTMLCanvasElement) &&\n !(texture instanceof HTMLVideoElement);\n };\n\n // Patch _isBrowserInterface to include our new method\n const originalIsBrowserInterface = GraphicsDevice.prototype._isBrowserInterface;\n if (originalIsBrowserInterface) {\n GraphicsDevice.prototype._isBrowserInterface = function(texture: any): boolean {\n return originalIsBrowserInterface.call(this, texture) ||\n this._isHTMLElementInterface(texture);\n };\n }\n\n console.log('[HtmlMeshHelper] Patched PlayCanvas GraphicsDevice for HTML-in-Canvas support');\n}\n\n/**\n * HTML Mesh configuration\n */\nexport interface HtmlMeshConfig {\n /** Unique ID for the mesh */\n id: string;\n /** HTML content to render */\n html: string;\n /** CSS styles for the container (can reference external stylesheets) */\n css?: string;\n /** Position in world space */\n position: { x: number; y: number; z: number };\n /** Rotation in degrees */\n rotation?: { x: number; y: number; z: number };\n /** Scale of the mesh */\n scale?: { x: number; y: number; z: number };\n /** Width in pixels (default: 512) */\n width?: number;\n /** Height in pixels (default: 512) */\n height?: number;\n /** Whether to update the texture every frame (for animations) */\n animated?: boolean;\n /** Update rate in milliseconds (default: 100ms for animated) */\n updateRate?: number;\n /** Whether to face the camera (billboard mode) */\n billboard?: boolean;\n /** Billboard range control (percentage or waypoint based) */\n billboardRange?: {\n type: 'percentage' | 'waypoint';\n start: number;\n end: number;\n };\n /** Visibility range control */\n visibilityRange?: {\n type: 'percentage' | 'waypoint';\n start: number;\n end: number;\n };\n /** Whether to receive shadows */\n receiveShadows?: boolean;\n /** Whether to cast shadows */\n castShadows?: boolean;\n /** Opacity (0-1) */\n opacity?: number;\n /** Whether the mesh is double-sided */\n doubleSided?: boolean;\n}\n\n/**\n * HTML Mesh instance\n */\nexport interface HtmlMeshInstance {\n entity: pc.Entity;\n texture: pc.Texture;\n material: pc.StandardMaterial;\n htmlElement: HTMLElement;\n config: HtmlMeshConfig;\n destroy: () => void;\n update: () => void;\n}\n\n/**\n * Creates and manages HTML meshes in a PlayCanvas scene\n */\nexport class HtmlMeshManager {\n private app: pc.Application;\n private canvas: HTMLCanvasElement;\n private meshes: Map<string, HtmlMeshInstance> = new Map();\n private updateHandler: (() => void) | null = null;\n private useTexElement2D: boolean = false;\n\n constructor(app: pc.Application) {\n this.app = app;\n this.canvas = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n // Apply runtime patch to PlayCanvas for HTML-in-Canvas support\n // This ensures _isHTMLElementInterface exists even if the bundled build is used\n patchPlayCanvasForHtmlMesh();\n\n // Check if texElement2D is supported\n const device = app.graphicsDevice as any;\n this.useTexElement2D = device.supportsTexElement2D === true;\n\n console.log(`[HtmlMeshManager] texElement2D support: ${this.useTexElement2D}`);\n }\n\n /**\n * Create an HTML mesh from config\n */\n createMesh(config: HtmlMeshConfig): HtmlMeshInstance {\n const width = config.width || 512;\n const height = config.height || 512;\n\n // Create HTML element container\n const htmlElement = this.createHtmlElement(config, width, height);\n\n // Create texture\n const texture = this.createTexture(htmlElement, width, height, config);\n\n // Create material\n const material = this.createMaterial(texture, config);\n\n // Create entity with plane mesh\n const entity = this.createEntity(config, material, width, height);\n\n // Setup update logic if animated\n const instance: HtmlMeshInstance = {\n entity,\n texture,\n material,\n htmlElement,\n config,\n destroy: () => this.destroyMesh(config.id),\n update: () => this.updateMeshTexture(config.id)\n };\n\n this.meshes.set(config.id, instance);\n\n // Setup animation updates if needed\n if (config.animated && !this.updateHandler) {\n this.startUpdateLoop();\n }\n\n return instance;\n }\n\n /**\n * Create the HTML element for the mesh\n */\n private createHtmlElement(config: HtmlMeshConfig, width: number, height: number): HTMLElement {\n const container = document.createElement('div');\n container.id = `html-mesh-${config.id}`;\n container.style.width = `${width}px`;\n container.style.height = `${height}px`;\n container.style.position = 'absolute';\n container.style.top = '0';\n container.style.left = '0';\n container.style.pointerEvents = 'none';\n container.style.zIndex = '-1';\n container.style.overflow = 'hidden';\n\n // Add custom CSS if provided\n if (config.css) {\n const style = document.createElement('style');\n style.textContent = config.css;\n container.appendChild(style);\n }\n\n // Set HTML content\n container.innerHTML += config.html;\n\n // For texElement2D, the element must be a child of the canvas\n if (this.useTexElement2D) {\n // Enable layoutsubtree for HTML-in-Canvas support\n this.canvas.setAttribute('layoutsubtree', '');\n this.canvas.setAttribute('data-layoutsubtree', '');\n this.canvas.appendChild(container);\n } else {\n // For fallback, we can keep it hidden in the body\n container.style.visibility = 'hidden';\n document.body.appendChild(container);\n }\n\n return container;\n }\n\n /**\n * Create texture from HTML element\n */\n private createTexture(htmlElement: HTMLElement, width: number, height: number, config: HtmlMeshConfig): pc.Texture {\n const texture = new pc.Texture(this.app.graphicsDevice, {\n width,\n height,\n format: pc.PIXELFORMAT_RGBA8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE,\n name: `htmlMesh-${config.id}`\n });\n\n // Set the HTML element as the texture source\n if (this.useTexElement2D) {\n try {\n texture.setSource(htmlElement as any);\n console.log(`[HtmlMeshManager] Using texElement2D for mesh ${config.id}`);\n } catch (error) {\n console.warn(`[HtmlMeshManager] texElement2D failed, falling back to canvas: ${error}`);\n this.renderToCanvas(texture, htmlElement, width, height);\n }\n } else {\n this.renderToCanvas(texture, htmlElement, width, height);\n }\n\n return texture;\n }\n\n /**\n * Fallback: Render HTML to canvas and use as texture source\n */\n private renderToCanvas(texture: pc.Texture, htmlElement: HTMLElement, width: number, height: number): void {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d', { willReadFrequently: true })!;\n\n // Create an SVG foreignObject containing the HTML\n const svg = `\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\">\n <foreignObject width=\"100%\" height=\"100%\">\n <div xmlns=\"http://www.w3.org/1999/xhtml\" style=\"width:${width}px;height:${height}px;\">\n ${htmlElement.innerHTML}\n </div>\n </foreignObject>\n </svg>\n `;\n\n const img = new Image();\n const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n\n img.onload = () => {\n ctx.drawImage(img, 0, 0);\n URL.revokeObjectURL(url);\n texture.setSource(canvas);\n };\n\n img.onerror = () => {\n // Fallback: just draw a colored rectangle with text\n ctx.fillStyle = '#333';\n ctx.fillRect(0, 0, width, height);\n ctx.fillStyle = '#fff';\n ctx.font = '20px Arial';\n ctx.textAlign = 'center';\n ctx.fillText('HTML Mesh', width / 2, height / 2);\n URL.revokeObjectURL(url);\n texture.setSource(canvas);\n };\n\n img.src = url;\n }\n\n /**\n * Create material for the mesh\n */\n private createMaterial(texture: pc.Texture, config: HtmlMeshConfig): pc.StandardMaterial {\n const material = new pc.StandardMaterial();\n material.diffuseMap = texture;\n material.emissiveMap = texture;\n material.emissive = new pc.Color(0.5, 0.5, 0.5);\n material.opacity = config.opacity ?? 1;\n material.blendType = config.opacity !== undefined && config.opacity < 1 ? pc.BLEND_NORMAL : pc.BLEND_NONE;\n material.cull = config.doubleSided ? pc.CULLFACE_NONE : pc.CULLFACE_BACK;\n material.update();\n return material;\n }\n\n /**\n * Create entity with plane mesh\n */\n private createEntity(config: HtmlMeshConfig, material: pc.StandardMaterial, width: number, height: number): pc.Entity {\n const entity = new pc.Entity(`htmlMesh-${config.id}`);\n\n // Calculate aspect ratio for the plane\n const aspect = width / height;\n const baseSize = 1;\n\n entity.addComponent('render', {\n type: 'plane',\n material,\n castShadows: config.castShadows ?? false,\n receiveShadows: config.receiveShadows ?? false\n });\n\n // Set transform\n entity.setPosition(config.position.x, config.position.y, config.position.z);\n\n const rotation = config.rotation || { x: 0, y: 0, z: 0 };\n entity.setEulerAngles(rotation.x, rotation.y, rotation.z);\n\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n entity.setLocalScale(scale.x * aspect, 1, scale.y);\n\n this.app.root.addChild(entity);\n\n // Setup billboard if requested\n if (config.billboard) {\n this.app.on('update', () => {\n if (!entity.enabled) return;\n const camera = this.app.root.findComponent('camera')?.entity;\n if (camera) {\n entity.lookAt(camera.getPosition());\n }\n });\n }\n\n return entity;\n }\n\n /**\n * Update a mesh's texture\n */\n updateMeshTexture(id: string): void {\n const instance = this.meshes.get(id);\n if (!instance) return;\n\n if (this.useTexElement2D) {\n // For texElement2D, just call upload to refresh\n instance.texture.upload();\n } else {\n // For canvas fallback, re-render\n this.renderToCanvas(\n instance.texture,\n instance.htmlElement,\n instance.config.width || 512,\n instance.config.height || 512\n );\n }\n }\n\n /**\n * Start the update loop for animated meshes\n */\n private startUpdateLoop(): void {\n let lastUpdate: { [id: string]: number } = {};\n\n this.updateHandler = () => {\n const now = performance.now();\n\n this.meshes.forEach((instance, id) => {\n if (!instance.config.animated) return;\n\n const rate = instance.config.updateRate || 100;\n const last = lastUpdate[id] || 0;\n\n if (now - last >= rate) {\n this.updateMeshTexture(id);\n lastUpdate[id] = now;\n }\n });\n };\n\n this.app.on('update', this.updateHandler);\n }\n\n /**\n * Destroy a mesh\n */\n destroyMesh(id: string): void {\n const instance = this.meshes.get(id);\n if (!instance) return;\n\n // Remove entity\n instance.entity.destroy();\n\n // Destroy texture\n instance.texture.destroy();\n\n // Remove HTML element\n instance.htmlElement.remove();\n\n this.meshes.delete(id);\n\n // Stop update loop if no animated meshes left\n const hasAnimated = Array.from(this.meshes.values()).some(m => m.config.animated);\n if (!hasAnimated && this.updateHandler) {\n this.app.off('update', this.updateHandler);\n this.updateHandler = null;\n }\n }\n\n /**\n * Get a mesh instance by ID\n */\n getMesh(id: string): HtmlMeshInstance | undefined {\n return this.meshes.get(id);\n }\n\n /**\n * Update visibility and billboard state based on progress\n * Called from the main viewer to sync with scroll/waypoint progress\n */\n updateVisibility(scrollPercent: number, waypointIndex: number): void {\n this.meshes.forEach((instance) => {\n const config = instance.config;\n\n // Update visibility based on range\n if (config.visibilityRange) {\n const range = config.visibilityRange;\n let visible = true;\n\n if (range.type === 'percentage') {\n visible = scrollPercent >= range.start && scrollPercent <= range.end;\n } else if (range.type === 'waypoint') {\n visible = waypointIndex >= range.start && waypointIndex <= range.end;\n }\n\n instance.entity.enabled = visible;\n }\n\n // Update billboard state based on range\n if (config.billboard && config.billboardRange) {\n const range = config.billboardRange;\n let billboardActive = false;\n\n if (range.type === 'percentage') {\n billboardActive = scrollPercent >= range.start && scrollPercent <= range.end;\n } else if (range.type === 'waypoint') {\n billboardActive = waypointIndex >= range.start && waypointIndex <= range.end;\n }\n\n // Store billboard active state for the update loop\n (instance.entity as any)._billboardActive = billboardActive;\n } else if (config.billboard) {\n // Billboard always active if no range\n (instance.entity as any)._billboardActive = true;\n }\n });\n }\n\n /**\n * Get all mesh instances\n */\n getAllMeshes(): Map<string, HtmlMeshInstance> {\n return this.meshes;\n }\n\n /**\n * Destroy all meshes and cleanup\n */\n destroy(): void {\n this.meshes.forEach((_, id) => this.destroyMesh(id));\n\n if (this.updateHandler) {\n this.app.off('update', this.updateHandler);\n this.updateHandler = null;\n }\n }\n}\n\n/**\n * Setup HTML meshes from config array\n */\nexport function setupHtmlMeshes(\n app: pc.Application,\n htmlMeshes: HtmlMeshConfig[]\n): HtmlMeshManager {\n const manager = new HtmlMeshManager(app);\n\n for (const config of htmlMeshes) {\n manager.createMesh(config);\n }\n\n return manager;\n}\n","/**\n * Custom Script System for PlayCanvas\n *\n * Provides runtime execution of user-defined scripts in the viewer.\n * Adapted from the HTML export system for PlayCanvas engine.\n */\n\nimport * as pc from 'playcanvas';\n\n/**\n * API exposed to custom scripts\n */\nexport interface CustomScriptAPI {\n /** PlayCanvas Application instance */\n app: pc.Application;\n /** Camera entity */\n camera: pc.Entity;\n /** PlayCanvas namespace */\n pc: typeof pc;\n /** Canvas element */\n canvas: HTMLCanvasElement;\n /** Get current scroll/progress percentage (0-1) */\n getScrollPercentage: () => number;\n /** Get current waypoint index */\n getCurrentWaypointIndex: () => number;\n /** Get all hotspot entities */\n getHotspots: () => pc.Entity[];\n /** Get splat mesh entities */\n getSplats: () => pc.Entity[];\n /** Get HTML mesh instances */\n getHTMLMeshes: () => any[];\n /** Register a cleanup function to be called on script disposal */\n registerCleanup?: (fn: () => void) => void;\n}\n\n/**\n * Custom Script System\n *\n * Executes user-provided scripts with access to the viewer API.\n * Includes safety measures like sandboxing and cleanup tracking.\n */\nexport class CustomScriptSystem {\n private customScript: string;\n private api: CustomScriptAPI;\n private isInitialized: boolean = false;\n private scriptCleanup: (() => void)[] = [];\n private lastError: any = null;\n private updateCallbacks: (() => void)[] = [];\n\n constructor(customScript: string) {\n this.customScript = customScript;\n this.api = {} as CustomScriptAPI;\n }\n\n /**\n * Initialize the script system with the viewer API\n */\n initialize(api: CustomScriptAPI): void {\n this.api = {\n ...api,\n registerCleanup: (fn: () => void) => this.addCleanup(fn)\n };\n this.isInitialized = true;\n }\n\n /**\n * Update the script content and re-execute\n */\n updateScript(script: string): void {\n if (script === this.customScript) return;\n this.customScript = script;\n this.execute();\n }\n\n /**\n * Add a cleanup function to be called on disposal\n */\n private addCleanup(fn: () => void): void {\n if (typeof fn === 'function') {\n this.scriptCleanup.push(fn);\n }\n }\n\n /**\n * Sanitize script to remove problematic characters\n */\n private sanitizeScript(script: string): string {\n if (!script) return '';\n let s = typeof (script as any).normalize === 'function' ? (script as any).normalize('NFC') : script;\n // Remove BOM\n s = s.replace(/\\uFEFF/g, '');\n // Replace non-breaking space with regular space\n s = s.replace(/\\u00A0/g, ' ');\n // Replace unicode line/paragraph separators with newlines\n s = s.replace(/[\\u2028\\u2029]/g, '\\n');\n // Remove zero-width characters\n s = s.replace(/[\\u200B-\\u200D\\u2060]/g, '');\n // Normalize curly quotes\n s = s.replace(/[\\u2018\\u2019\\u201B]/g, \"'\").replace(/[\\u201C\\u201D\\u201E]/g, '\"');\n return s;\n }\n\n /**\n * Preprocess script with safety wrapping\n */\n private preprocessScript(script: string): string {\n if (!script) return '';\n let processed = this.sanitizeScript(script).trim().replace(/\\r\\n/g, '\\n');\n\n // Warn about common typo\n if (/console\\/log\\s*\\(/.test(processed)) {\n console.warn(\"[Custom Script] Detected 'console/log(...)'. Did you mean 'console.log(...)'?\");\n processed = processed.replace(/console\\/log\\s*\\(/g, 'console.log(');\n }\n\n // Wrap in try-catch for safety\n processed = \"try {\\n\" + processed + \"\\n} catch (error) {\\n console.error('[Custom Script] Runtime error:', error);\\n}\";\n return processed;\n }\n\n /**\n * Execute the custom script\n */\n execute(): void {\n if (!this.isInitialized || !this.customScript) {\n return;\n }\n\n // Prevent extremely large scripts\n if (this.customScript.length > 200_000) {\n console.warn('[Custom Script] Script too large, aborting execution.');\n return;\n }\n\n this.cleanup();\n const processedScript = this.preprocessScript(this.customScript);\n\n try {\n const app = this.api.app;\n let cancelled = false;\n\n // Watchdog to prevent runaway update callbacks\n const watchdog = () => {\n if (cancelled) return;\n if (this.updateCallbacks.length > 200) {\n console.warn('[Custom Script] Too many update callbacks; further callbacks ignored.');\n cancelled = true;\n } else {\n requestAnimationFrame(watchdog);\n }\n };\n requestAnimationFrame(watchdog);\n\n // Wrapped app with tracked registerUpdate (PlayCanvas equivalent of registerBeforeRender)\n const wrappedApp = Object.create(app);\n wrappedApp.registerUpdate = (callback: () => void) => {\n if (typeof callback !== 'function' || cancelled) return;\n const safeCallback = () => {\n try {\n callback();\n } catch (err) {\n console.error('[Custom Script] Error in update callback:', err);\n }\n };\n this.updateCallbacks.push(safeCallback);\n app.on('update', safeCallback);\n this.addCleanup(() => {\n app.off('update', safeCallback);\n const idx = this.updateCallbacks.indexOf(safeCallback);\n if (idx !== -1) this.updateCallbacks.splice(idx, 1);\n });\n };\n\n // Also support BabylonJS-style API for compatibility\n wrappedApp.registerBeforeRender = wrappedApp.registerUpdate;\n\n // Build limited API passed to user script\n const {\n camera,\n pc: pcNamespace,\n canvas,\n getScrollPercentage,\n getCurrentWaypointIndex,\n getHotspots,\n getSplats,\n getHTMLMeshes\n } = this.api;\n\n // Build the function body\n const body = \"'use strict';\\n\" + processedScript;\n\n // Create the function with sandboxed parameters\n // eslint-disable-next-line no-new-func\n const func = new Function(\n 'app',\n 'camera',\n 'pc',\n 'canvas',\n 'getScrollPercentage',\n 'getCurrentWaypointIndex',\n 'getHotspots',\n 'getSplats',\n 'getHTMLMeshes',\n 'registerCleanup',\n 'registerUpdate',\n 'exports',\n 'module',\n 'require',\n 'globalThis',\n 'window',\n 'document',\n 'self',\n body\n );\n\n // Provide restricted globals (prevent direct window access by shadowing)\n const exports: any = {};\n const module: any = { exports };\n const fakeRequire = () => { throw new Error('require not available in custom script'); };\n const blocked = new Proxy({}, { get: () => undefined, set: () => false });\n\n const possibleReturn = func(\n wrappedApp,\n camera,\n pcNamespace,\n canvas,\n getScrollPercentage,\n getCurrentWaypointIndex,\n getHotspots,\n getSplats,\n getHTMLMeshes,\n (fn: () => void) => this.addCleanup(fn),\n wrappedApp.registerUpdate.bind(wrappedApp),\n exports,\n module,\n fakeRequire,\n blocked,\n undefined,\n undefined,\n undefined\n );\n\n // Check for returned cleanup function\n const cleanupCandidate = possibleReturn || module.exports || exports.default || exports.cleanup;\n if (typeof cleanupCandidate === 'function') {\n this.addCleanup(cleanupCandidate);\n }\n\n console.log('[Custom Script] Executed successfully');\n } catch (error) {\n this.lastError = error;\n console.error('[Custom Script] Execution error:', error);\n }\n }\n\n /**\n * Run all cleanup functions\n */\n cleanup(): void {\n this.scriptCleanup.forEach(fn => {\n try {\n fn();\n } catch (error) {\n console.error('[Custom Script] Cleanup error:', error);\n }\n });\n this.scriptCleanup = [];\n this.updateCallbacks = [];\n }\n\n /**\n * Get the last error that occurred during execution\n */\n getLastError(): any {\n return this.lastError;\n }\n\n /**\n * Dispose of the script system and cleanup\n */\n dispose(): void {\n this.cleanup();\n this.isInitialized = false;\n }\n}\n\n/**\n * Setup custom script system with the viewer\n */\nexport function setupCustomScript(\n app: pc.Application,\n camera: pc.Entity,\n canvas: HTMLCanvasElement,\n customScript: string,\n getScrollPercentage: () => number,\n getCurrentWaypointIndex: () => number,\n getHotspots: () => pc.Entity[],\n getSplats: () => pc.Entity[],\n getHTMLMeshes: () => any[]\n): CustomScriptSystem | null {\n if (!customScript || customScript.trim() === '') {\n console.log('[Custom Script] No custom script provided');\n return null;\n }\n\n console.log('[Custom Script] Initializing custom script system...');\n console.log('[Custom Script] Script length:', customScript.length);\n\n const scriptSystem = new CustomScriptSystem(customScript);\n\n scriptSystem.initialize({\n app,\n camera,\n pc,\n canvas,\n getScrollPercentage,\n getCurrentWaypointIndex,\n getHotspots,\n getSplats,\n getHTMLMeshes\n });\n\n scriptSystem.execute();\n\n return scriptSystem;\n}\n","/**\n * Dynamic Viewer for StorySplat\n *\n * Creates an embedded PlayCanvas viewer from scene data.\n * Uses the same core logic as HTML export for 100% parity.\n */\n\nimport * as pc from 'playcanvas';\nimport { transformSceneToExportProps, exportPropsToViewerConfig } from '../transformers/sceneToConfig';\nimport type { SceneData, ViewerInstance, ViewerOptions, ViewerEvent } from '../types';\nimport { createUIElements, connectUIToViewer, injectStyles, hidePreloader, updatePreloaderProgress, showHotspotPopup, setJoystickVisible, updateJoystickPosition, setCameraModeToggleVisible, updateCameraModeToggle, setReturnWaypointButtonVisible, createLazyLoadUI, type UIElements } from './viewerUI';\nimport { CameraControls } from './CameraControls';\nimport { CharacterController } from './CharacterController';\nimport { getGsplatRevealRadialClass, getRevealPreset, type RevealPreset } from '../effects';\nimport { AnimatedGifTexture, isGifUrl } from './AnimatedGifHelper';\nimport { setupHtmlMeshes, type HtmlMeshConfig } from './HtmlMeshHelper';\nimport { setupCustomScript, CustomScriptSystem } from './CustomScriptSystem';\n\n// Event emitter for viewer events\nclass EventEmitter {\n private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();\n\n on(event: string, callback: (...args: any[]) => void): void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(callback);\n }\n\n off(event: string, callback: (...args: any[]) => void): void {\n this.listeners.get(event)?.delete(callback);\n }\n\n emit(event: string, ...args: any[]): void {\n this.listeners.get(event)?.forEach(cb => cb(...args));\n }\n}\n\n// Mobile detection utility\nfunction isMobileDevice(): boolean {\n // Only check user agent - don't use maxTouchPoints as many laptops have touch\n const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera || '';\n return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);\n}\n\n// LOD (Level of Detail) presets for different device capabilities\n// Based on official PlayCanvas GSplat LOD streaming example\nconst LOD_PRESETS = {\n 'desktop-max': {\n range: [0, 5] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n },\n 'desktop': {\n range: [0, 2] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n },\n 'mobile-max': {\n range: [1, 2] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n },\n 'mobile': {\n range: [2, 5] as [number, number],\n lodDistances: [15, 30, 80, 250, 300]\n }\n} as const;\n\ntype LodPresetName = keyof typeof LOD_PRESETS;\n\n// Helper to detect if URL points to LOD streaming format\nfunction isLodStreamingFormat(url: string): boolean {\n return url.includes('lod-meta.json');\n}\n\n/**\n * Create an embedded viewer from scene data\n */\nexport function createViewer(\n container: HTMLElement,\n scene: SceneData,\n options: ViewerOptions = {}\n): ViewerInstance {\n // Handle lazy loading - show thumbnail with start button before loading viewer\n if (options.lazyLoad) {\n const lazyEvents = new EventEmitter();\n let actualInstance: ViewerInstance | null = null;\n\n // Determine thumbnail URL and button text\n const thumbnailUrl = options.lazyLoadThumbnail || scene.thumbnailUrl;\n const buttonText = options.lazyLoadButtonText || 'Start Experience';\n const uiColor = scene.uiColor || '#4CAF50';\n\n console.log('[StorySplat Viewer] Lazy loading enabled, showing start button');\n\n // Create lazy load UI\n createLazyLoadUI(container, {\n thumbnailUrl,\n buttonText,\n uiColor,\n onStart: () => {\n console.log('[StorySplat Viewer] User clicked start, initializing viewer...');\n // Create actual viewer without lazy loading\n actualInstance = createViewer(container, scene, { ...options, lazyLoad: false });\n\n // Proxy events from actual instance to lazy instance\n actualInstance.on('ready', () => lazyEvents.emit('ready'));\n actualInstance.on('error', (err) => lazyEvents.emit('error', err));\n actualInstance.on('waypointChange', (data) => lazyEvents.emit('waypointChange', data));\n actualInstance.on('playbackStart', () => lazyEvents.emit('playbackStart'));\n actualInstance.on('playbackStop', () => lazyEvents.emit('playbackStop'));\n actualInstance.on('loaded', () => lazyEvents.emit('loaded'));\n actualInstance.on('progress', (data) => lazyEvents.emit('progress', data));\n }\n });\n\n // Return deferred ViewerInstance - methods delegate to actual instance when available\n const deferredInstance: ViewerInstance = {\n app: null as any,\n canvas: null as any,\n\n goToWaypoint: (index) => actualInstance?.goToWaypoint(index),\n nextWaypoint: () => actualInstance?.nextWaypoint(),\n prevWaypoint: () => actualInstance?.prevWaypoint(),\n getCurrentWaypointIndex: () => actualInstance?.getCurrentWaypointIndex() ?? 0,\n getWaypointCount: () => actualInstance?.getWaypointCount() ?? 0,\n\n setPosition: (x, y, z) => actualInstance?.setPosition(x, y, z),\n setRotation: (x, y, z) => actualInstance?.setRotation(x, y, z),\n getPosition: () => actualInstance?.getPosition() ?? { x: 0, y: 0, z: 0 },\n getRotation: () => actualInstance?.getRotation() ?? { x: 0, y: 0, z: 0 },\n\n play: () => actualInstance?.play(),\n pause: () => actualInstance?.pause(),\n stop: () => actualInstance?.stop(),\n isPlaying: () => actualInstance?.isPlaying() ?? false,\n\n destroy: () => {\n if (actualInstance) {\n actualInstance.destroy();\n } else {\n // Clean up lazy load UI if viewer wasn't started\n container.querySelectorAll('.storysplat-lazy-load-container, .storysplat-viewer-container').forEach(el => el.remove());\n container.classList.remove('storysplat-viewer-container');\n }\n },\n resize: () => actualInstance?.resize(),\n\n navigateToScene: async (sceneId) => {\n if (actualInstance) {\n return actualInstance.navigateToScene(sceneId);\n }\n },\n\n on: (event, callback) => lazyEvents.on(event, callback),\n off: (event, callback) => lazyEvents.off(event, callback)\n };\n\n return deferredInstance;\n }\n\n const events = new EventEmitter();\n const config = exportPropsToViewerConfig(transformSceneToExportProps(scene));\n\n console.log('[StorySplat Viewer] Creating viewer with config:', config);\n console.log('[StorySplat Viewer] Scale config:', config.scale);\n console.log('[StorySplat Viewer] Raw scene data:', { splatScale: scene.splatScale, scale: scene.scale });\n\n // Extract UI options from config (respecting JSON settings)\n const showUI = options.showUI !== false; // Default to true\n const uiColor = config.uiColor || '#4CAF50';\n const uiOpts = config.uiOptions || {};\n\n // Map camera modes from export format to UI format\n const mapCameraMode = (mode: string) => {\n if (mode === 'first-person') return 'tour';\n if (mode === 'drone') return 'explore';\n return mode;\n };\n\n // Check if collision meshes are available for walk mode\n const hasCollisionMeshesForWalk = config.collisionMeshesData && config.collisionMeshesData.length > 0;\n\n let allowedModes = (config.allowedCameraModes || ['orbit', 'first-person', 'drone'])\n .map(mapCameraMode)\n .filter((v, i, a) => a.indexOf(v) === i); // unique\n\n // Add 'walk' mode if collision meshes are available and it's in the allowed modes list\n // OR automatically add it if collision meshes exist (for backwards compatibility)\n if (hasCollisionMeshesForWalk && !allowedModes.includes('walk')) {\n allowedModes.push('walk');\n }\n // Remove 'walk' if no collision meshes (can't walk without collisions)\n if (!hasCollisionMeshesForWalk && allowedModes.includes('walk')) {\n allowedModes = allowedModes.filter(m => m !== 'walk');\n }\n\n const defaultMode = mapCameraMode(config.defaultCameraMode || 'orbit');\n\n let uiElements: UIElements = {};\n\n // Create UI elements FIRST (to show preloader while loading)\n if (showUI) {\n uiElements = createUIElements(container, config, {\n uiColor,\n showScrollControls: config.waypoints && config.waypoints.length > 0,\n showModeToggle: allowedModes.length > 1,\n showFullscreenButton: !uiOpts.hideFullscreenButton,\n showHelpButton: !uiOpts.hideHelpButton && !uiOpts.hideInfoButton,\n showPreloader: true,\n allowedCameraModes: allowedModes,\n defaultCameraMode: defaultMode,\n buttonLabels: uiOpts.buttonLabels,\n // Whitelabeling options for preloader\n customPreloaderLogoUrl: uiOpts.customPreloaderLogoUrl,\n // Whitelabeling options for watermark\n hideWatermark: uiOpts.hideWatermark,\n watermarkText: uiOpts.watermarkText,\n watermarkLink: uiOpts.watermarkLink,\n sceneId: scene.sceneId\n });\n }\n\n // Create canvas\n const canvas = document.createElement('canvas');\n canvas.id = 'storysplat-viewer-canvas';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n canvas.style.display = 'block';\n container.appendChild(canvas);\n\n // Initialize PlayCanvas with WebGL fallback chain\n // Try WebGL2 first, then WebGL1 if that fails\n let app: pc.Application;\n\n // Graphics device options with fallback support\n const baseGraphicsOptions = {\n antialias: false,\n alpha: false,\n powerPreference: 'high-performance' as const\n };\n\n try {\n // First try with default settings (WebGL2 preferred)\n app = new pc.Application(canvas, {\n graphicsDeviceOptions: baseGraphicsOptions,\n mouse: new pc.Mouse(canvas),\n touch: new pc.TouchDevice(canvas),\n keyboard: new pc.Keyboard(window)\n });\n console.log('[StorySplat Viewer] Graphics initialized successfully');\n } catch (webgl2Error) {\n console.warn('[StorySplat Viewer] WebGL2 initialization failed, trying WebGL1 fallback:', webgl2Error);\n\n try {\n // Fallback to WebGL1\n app = new pc.Application(canvas, {\n graphicsDeviceOptions: {\n ...baseGraphicsOptions,\n preferWebGl2: false\n },\n mouse: new pc.Mouse(canvas),\n touch: new pc.TouchDevice(canvas),\n keyboard: new pc.Keyboard(window)\n });\n console.log('[StorySplat Viewer] WebGL1 fallback successful');\n } catch (webgl1Error) {\n console.error('[StorySplat Viewer] WebGL initialization failed completely:', webgl1Error);\n\n // Show user-friendly error message using safe DOM methods\n const errorDiv = document.createElement('div');\n errorDiv.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:rgba(0,0,0,0.8);padding:20px;border-radius:10px;text-align:center;font-family:sans-serif;';\n\n const heading = document.createElement('h3');\n heading.style.cssText = 'margin:0 0 10px 0;';\n heading.textContent = 'Unable to Initialize 3D Graphics';\n\n const message = document.createElement('p');\n message.style.cssText = 'margin:0;';\n message.textContent = 'Your browser or device may not support WebGL. Please try a different browser or device.';\n\n errorDiv.appendChild(heading);\n errorDiv.appendChild(message);\n container.appendChild(errorDiv);\n\n throw new Error('WebGL initialization failed - browser may not support WebGL');\n }\n }\n\n // Handle WebGL context loss\n canvas.addEventListener('webglcontextlost', (e) => {\n e.preventDefault();\n console.error('[StorySplat Viewer] WebGL context lost');\n events.emit('error', new Error('WebGL context lost'));\n }, false);\n\n canvas.addEventListener('webglcontextrestored', () => {\n console.log('[StorySplat Viewer] WebGL context restored');\n // PlayCanvas should handle context restoration automatically\n }, false);\n\n app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);\n app.setCanvasResolution(pc.RESOLUTION_AUTO);\n\n // Start the app first (before adding entities)\n app.start();\n console.log('[StorySplat Viewer] App started');\n\n // Configure GSplat LOD system for optimal performance\n // This improves rendering for all splat formats (PLY, SOG, LOD)\n const isMobile = isMobileDevice();\n const lodPresetName: LodPresetName = isMobile ? 'mobile' : 'desktop';\n const lodPreset = LOD_PRESETS[lodPresetName];\n\n // Scene-level GSplat settings (applies to all gsplat components)\n if (app.scene.gsplat) {\n // LOD update settings - controls how frequently LOD is recalculated\n app.scene.gsplat.lodUpdateAngle = 90; // Angle change threshold for LOD update\n app.scene.gsplat.lodBehindPenalty = 2; // Penalty multiplier for splats behind camera\n app.scene.gsplat.radialSorting = true; // Enable radial sorting for better quality\n app.scene.gsplat.lodUpdateDistance = 1; // Distance change threshold for LOD update\n app.scene.gsplat.lodUnderfillLimit = 10; // Limit for underfill detection\n\n // LOD range settings - controls which LOD levels are used\n app.scene.gsplat.lodRangeMin = lodPreset.range[0];\n app.scene.gsplat.lodRangeMax = lodPreset.range[1];\n\n // SH (Spherical Harmonics) update settings for better color accuracy\n app.scene.gsplat.colorUpdateDistance = 1;\n app.scene.gsplat.colorUpdateAngle = 4;\n app.scene.gsplat.colorUpdateDistanceLodScale = 2;\n app.scene.gsplat.colorUpdateAngleLodScale = 2;\n\n console.log('[StorySplat Viewer] GSplat LOD configured:', {\n preset: lodPresetName,\n lodRange: lodPreset.range,\n isMobile\n });\n }\n\n // State\n let currentWaypointIndex = 0;\n let isPlaying = false;\n let splatEntity: pc.Entity | null = null;\n let revealScript: any = null; // Reference to reveal effect script\n let isDestroyed = false; // Flag to prevent async operations after destroy\n\n // SplatSwap system state\n const additionalSplats = config.additionalSplats || [];\n const keepMeshesInMemory = config.keepMeshesInMemory ?? false;\n let currentSplatUrl: string | null = null;\n let isLoadingSplat = false;\n let initialSplatLoadDone = false;\n const preloadedSplats = new Map<string, pc.Entity>(); // Map URL -> splat entity\n let lastSplatCheckProgress = -1;\n let lastSplatCheckWaypointIndex = -1;\n\n // Create camera (matching HTML export CONFIG values)\n const camera = new pc.Entity('camera');\n\n // Parse background color from config (supports hex strings like \"#1a1a1a\")\n // Default to dark gray (0.1, 0.1, 0.1) if not specified\n let clearColor = new pc.Color(0.1, 0.1, 0.1);\n if (config.backgroundColor) {\n // Parse hex color string to RGB\n const hex = config.backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16) / 255;\n const g = parseInt(hex.substring(2, 4), 16) / 255;\n const b = parseInt(hex.substring(4, 6), 16) / 255;\n clearColor = new pc.Color(r, g, b);\n console.log('[StorySplat Viewer] Background color set from config:', config.backgroundColor);\n }\n }\n\n camera.addComponent('camera', {\n clearColor,\n fov: config.fov || 60,\n nearClip: config.nearClip || 0.1,\n farClip: config.farClip || 1000\n });\n\n // Add audio listener for spatial audio (required for positional sound)\n camera.addComponent('audiolistener');\n\n console.log('[StorySplat Viewer] Camera settings:', {\n fov: config.fov,\n nearClip: config.nearClip,\n farClip: config.farClip,\n playerHeight: config.playerHeight\n });\n\n // Set initial camera position - matching HTML export behavior\n // If waypoints exist, set camera DIRECTLY to first waypoint to avoid 1-frame flash\n const playerHeight = config.playerHeight || 1.6;\n\n if (config.waypoints && config.waypoints.length > 0) {\n const wp = config.waypoints[0];\n console.log('[StorySplat Viewer] First waypoint raw:', wp);\n\n // Set position SYNCHRONOUSLY to avoid camera flash\n // Convert BabylonJS (left-handed) to PlayCanvas (right-handed) - negate Z\n if (wp.position) {\n const pos = wp.position;\n camera.setPosition(pos.x, pos.y, -pos.z);\n console.log('[StorySplat Viewer] Camera position (Z negated):', { x: pos.x, y: pos.y, z: -pos.z });\n } else {\n camera.setPosition(0, playerHeight, 5);\n }\n\n // Set rotation SYNCHRONOUSLY\n if (wp.rotation) {\n const finalRot = convertWaypointRotation(wp.rotation);\n camera.setRotation(finalRot);\n console.log('[StorySplat Viewer] Camera rotation set from waypoint');\n }\n } else {\n // No waypoints - use default position\n camera.setPosition(0, playerHeight, 5);\n camera.lookAt(new pc.Vec3(0, 0, 0));\n console.log('[StorySplat Viewer] Camera set to default position (0, playerHeight, 5)');\n }\n\n app.root.addChild(camera);\n\n // Add directional light (matching HTML export)\n const light = new pc.Entity('light');\n light.addComponent('light', {\n type: pc.LIGHTTYPE_DIRECTIONAL,\n color: new pc.Color(1, 1, 1),\n intensity: 1,\n castShadows: false\n });\n light.setEulerAngles(45, 45, 0);\n app.root.addChild(light);\n console.log('[StorySplat Viewer] Light added');\n\n // Create CameraControls for explore mode (orbit + fly + focus)\n // Movement and rotation speeds tuned down for smoother, more controlled camera\n const cameraControls = new CameraControls(camera, app, {\n moveSpeed: (config.cameraMovementSpeed || 1) * 2, // Reduced from 5x to 2x\n moveFastSpeed: (config.cameraMovementSpeed || 1) * 5, // Reduced from 10x to 5x\n moveSlowSpeed: (config.cameraMovementSpeed || 1) * 1, // Reduced from 2.5x to 1x\n rotateSpeed: (config.cameraRotationSensitivity || 0.2) * 0.0005, // Halved from 0.001 to 0.0005\n enableOrbit: true,\n enableFly: true,\n enablePan: true,\n invertRotation: config.invertCameraRotation\n });\n\n // Create CharacterController for walk mode (first-person with collisions)\n let characterController: CharacterController | null = null;\n const hasCollisionMeshes = config.collisionMeshesData && config.collisionMeshesData.length > 0;\n\n if (hasCollisionMeshes) {\n // CharacterController for walk mode - tuned down for smoother movement\n characterController = new CharacterController(camera, app, {\n moveSpeed: (config.cameraMovementSpeed || 1) * 2, // Reduced from 4x to 2x\n sprintMultiplier: 2,\n lookSensitivity: (config.cameraRotationSensitivity || 0.2) * 0.005, // Halved from 0.01 to 0.005\n playerHeight: config.playerHeight || 1.6,\n gravity: 20,\n jumpVelocity: 8,\n collisionRadius: 0.3,\n stepHeight: 0.3\n });\n\n // Load collision meshes asynchronously\n characterController.createCollisionMeshes(config.collisionMeshesData!).then(() => {\n console.log('[StorySplat Viewer] Collision meshes loaded for walk mode');\n }).catch((err) => {\n console.error('[StorySplat Viewer] Failed to load collision meshes:', err);\n });\n }\n\n // Track current camera mode\n let currentCameraMode = defaultMode;\n let waypointControlEnabled = true; // When false, waypoint navigation doesn't control camera\n\n // Per-waypoint orbit mode state (for tour mode with orbit waypoints)\n let currentWaypointOrbitEnabled = false;\n let currentWaypointOrbitTarget: pc.Vec3 | null = null;\n\n // Disable controls initially if in tour mode\n if (defaultMode === 'tour') {\n cameraControls.disable();\n }\n\n // Create picker for GSplat picking (double-click to focus)\n // Enable depth=true for getWorldPointAsync to work with splats (requires PlayCanvas 2.14+)\n const picker = new pc.Picker(app, 1, 1, true);\n\n // XR (VR/AR) Support\n let isInXR = false;\n let xrSessionType: 'vr' | 'ar' | null = null;\n\n // Setup XR if enabled in config\n function setupXR(): void {\n if (!config.includeXR) return;\n\n // Check if XR is supported\n if (!app.xr) {\n console.warn('[StorySplat Viewer] WebXR not supported in this browser');\n return;\n }\n\n const xr = app.xr; // Non-null reference for TypeScript\n const xrMode = config.xrMode || 'both';\n\n // Check VR availability\n if (xrMode === 'vr' || xrMode === 'both') {\n if (xr.isAvailable(pc.XRTYPE_VR)) {\n uiElements.vrButton?.classList.add('available');\n console.log('[StorySplat Viewer] VR is available');\n }\n\n // Listen for VR availability changes\n xr.on('available:' + pc.XRTYPE_VR, (available: boolean) => {\n if (available) {\n uiElements.vrButton?.classList.add('available');\n } else {\n uiElements.vrButton?.classList.remove('available');\n }\n });\n }\n\n // Check AR availability\n if (xrMode === 'ar' || xrMode === 'both') {\n if (xr.isAvailable(pc.XRTYPE_AR)) {\n uiElements.arButton?.classList.add('available');\n console.log('[StorySplat Viewer] AR is available');\n }\n\n // Listen for AR availability changes\n xr.on('available:' + pc.XRTYPE_AR, (available: boolean) => {\n if (available) {\n uiElements.arButton?.classList.add('available');\n } else {\n uiElements.arButton?.classList.remove('available');\n }\n });\n }\n\n // Handle XR session start\n xr.on('start', () => {\n isInXR = true;\n console.log('[StorySplat Viewer] XR session started');\n\n // Update button states\n if (xrSessionType === 'vr') {\n uiElements.vrButton?.classList.add('active');\n uiElements.vrButton!.textContent = 'Exit VR';\n } else if (xrSessionType === 'ar') {\n uiElements.arButton?.classList.add('active');\n uiElements.arButton!.textContent = 'Exit AR';\n }\n\n // Disable regular camera controls in XR\n cameraControls.disable();\n if (characterController) {\n characterController.disable();\n }\n\n events.emit('xrStart', { type: xrSessionType });\n });\n\n // Handle XR session end\n xr.on('end', () => {\n isInXR = false;\n console.log('[StorySplat Viewer] XR session ended');\n\n // Reset button states\n uiElements.vrButton?.classList.remove('active');\n uiElements.arButton?.classList.remove('active');\n if (uiElements.vrButton) uiElements.vrButton.textContent = 'VR';\n if (uiElements.arButton) uiElements.arButton.textContent = 'AR';\n\n xrSessionType = null;\n\n // Re-enable camera controls based on current mode\n if (currentCameraMode === 'explore') {\n cameraControls.enable();\n } else if (currentCameraMode === 'walk' && characterController) {\n characterController.enable();\n }\n\n events.emit('xrEnd', {});\n });\n\n // VR button click handler\n if (uiElements.vrButton) {\n uiElements.vrButton.addEventListener('click', () => {\n if (isInXR && xrSessionType === 'vr') {\n // Exit VR\n xr.end();\n } else if (!isInXR && xr.isAvailable(pc.XRTYPE_VR)) {\n // Enter VR\n xrSessionType = 'vr';\n camera.camera!.startXr(pc.XRTYPE_VR, pc.XRSPACE_LOCALFLOOR, {\n callback: (err: Error | null) => {\n if (err) {\n console.error('[StorySplat Viewer] Failed to start VR:', err);\n xrSessionType = null;\n }\n }\n });\n }\n });\n }\n\n // AR button click handler\n if (uiElements.arButton) {\n uiElements.arButton.addEventListener('click', () => {\n if (isInXR && xrSessionType === 'ar') {\n // Exit AR\n xr.end();\n } else if (!isInXR && xr.isAvailable(pc.XRTYPE_AR)) {\n // Enter AR\n xrSessionType = 'ar';\n camera.camera!.startXr(pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, {\n callback: (err: Error | null) => {\n if (err) {\n console.error('[StorySplat Viewer] Failed to start AR:', err);\n xrSessionType = null;\n }\n }\n });\n }\n });\n }\n }\n\n // Add update loop for CameraControls, CharacterController, and proximity checks\n app.on('update', (dt: number) => {\n // Update either CameraControls or CharacterController based on mode\n if (currentCameraMode === 'walk' && characterController) {\n characterController.update(dt);\n } else {\n cameraControls.update(dt);\n }\n\n // Check proximity triggers for video hotspots (needed for explore/walk mode)\n if (!waypointControlEnabled) {\n updateProximityTriggers();\n }\n\n // Update waypoint audio based on proximity (for spatial audio)\n updateWaypointAudioProximity();\n\n // Check waypoint trigger distances for interaction execution\n checkWaypointTriggerDistance();\n });\n\n // Listen to CameraControls joystick events for UI updates\n app.on('joystick:left', (bx: number, by: number, sx: number, sy: number) => {\n if (bx < 0 || by < 0) {\n // Joystick released\n updateJoystickPosition(uiElements, false, 0, 0, 60);\n } else {\n // Joystick active - calculate offset from base to stick\n const dx = sx - bx;\n const dy = sy - by;\n updateJoystickPosition(uiElements, true, dx, dy, 60);\n }\n });\n\n // Camera mode toggle button (Orbit/Fly) for mobile explore mode\n if (uiElements.cameraModeToggle) {\n uiElements.cameraModeToggle.addEventListener('click', () => {\n // Only toggle if in explore mode\n if (currentCameraMode !== 'explore') return;\n\n const currentMode = cameraControls.mode;\n if (currentMode === 'orbit' || currentMode === 'focus') {\n // Switch to fly mode - show joystick\n cameraControls.setMode('fly');\n updateCameraModeToggle(uiElements, 'fly');\n setJoystickVisible(uiElements, true);\n } else {\n // Switch to orbit mode - hide joystick (orbit uses touch gestures, not joystick)\n cameraControls.setMode('orbit');\n updateCameraModeToggle(uiElements, 'orbit');\n setJoystickVisible(uiElements, false);\n }\n });\n }\n\n // Listen for CameraControls internal mode changes (e.g., after double-tap focus)\n app.on('cameracontrols:modechange', (mode: string) => {\n // Update toggle button UI when mode changes internally\n if (mode === 'orbit' || mode === 'fly') {\n updateCameraModeToggle(uiElements, mode as 'orbit' | 'fly');\n // Also update joystick visibility on mobile\n if (isMobile && currentCameraMode === 'explore') {\n setJoystickVisible(uiElements, mode === 'fly');\n }\n }\n });\n\n // Proximity check function (called every frame in explore mode)\n function updateProximityTriggers(): void {\n const cameraPos = camera.getPosition();\n\n hotspotEntities.forEach((entity: any) => {\n const hotspot = entity.hotspotData;\n if (!hotspot || hotspot.type !== 'video' || !entity.videoElement) return;\n\n const triggerMode = entity.mediaTriggerMode || 'click';\n if (triggerMode !== 'proximity') return;\n\n const hotspotPos = entity.getPosition();\n const distance = cameraPos.distance(hotspotPos);\n const proximityDistance = entity.proximityDistance || 5;\n\n if (distance <= proximityDistance && !entity.isVideoPlaying) {\n playVideoHotspot(entity, hotspot);\n } else if (distance > proximityDistance && entity.isVideoPlaying) {\n pauseVideoHotspot(entity);\n }\n });\n }\n\n // Mode switching function (Tour, Explore, or Walk)\n function setCameraMode(mode: string): void {\n // Disable previous mode's controllers\n if (currentCameraMode === 'walk' && characterController) {\n characterController.disable();\n } else if (currentCameraMode === 'explore') {\n cameraControls.disable();\n }\n\n currentCameraMode = mode;\n console.log('[StorySplat Viewer] Switching to mode:', mode);\n\n const isMobile = isMobileDevice();\n\n if (mode === 'walk' && characterController) {\n // Walk mode: Enable CharacterController (first-person with collisions)\n waypointControlEnabled = false;\n\n // Disable CameraControls\n cameraControls.disable();\n\n // Enable CharacterController\n characterController.enable();\n\n // Hide joystick and camera mode toggle (CharacterController uses keyboard/mouse)\n setJoystickVisible(uiElements, false);\n setCameraModeToggleVisible(uiElements, false);\n\n // Show return to waypoint button (if we have waypoints)\n if (config.waypoints && config.waypoints.length > 0) {\n setReturnWaypointButtonVisible(uiElements, true);\n }\n } else if (mode === 'explore') {\n // Explore mode: Enable CameraControls (orbit + fly + pan)\n\n // Disable CharacterController if active\n if (characterController) {\n characterController.disable();\n }\n\n // CRITICAL: Reset user rotation offsets and disable waypoint control\n userYawOffset = 0;\n userPitchOffset = 0;\n isUserDragging = false;\n waypointControlEnabled = false;\n\n // IMPORTANT: Call disable() first to clear any accumulated input from tour mode dragging\n // The CameraControls input sources accumulate mouse/touch input even when disabled,\n // and this would cause an offset when we enable and the update loop reads that input\n cameraControls.disable(); // Clears input buffers\n cameraControls.enable();\n cameraControls.enableOrbit = true;\n cameraControls.enableFly = true;\n cameraControls.enablePan = true;\n\n // Use syncFromPose to directly set the camera from the waypoint target values\n // This bypasses the camera entity's current state which may have user rotation applied\n if (targetCameraPosition && targetCameraRotation) {\n cameraControls.syncFromPose(targetCameraPosition, targetCameraRotation);\n console.log('[StorySplat Viewer] Synced camera from waypoint pose for explore mode');\n } else {\n cameraControls.syncFromCamera();\n }\n\n // Try to find a better focus point using the picker (async improvement)\n const findBetterFocusPoint = async () => {\n try {\n const pickerScale = 0.25;\n picker.resize(\n Math.floor(canvasEl.clientWidth * pickerScale),\n Math.floor(canvasEl.clientHeight * pickerScale)\n );\n\n const worldLayer = app.scene.layers.getLayerByName('World');\n if (worldLayer) {\n picker.prepare(camera.camera!, app.scene, [worldLayer]);\n\n const centerX = Math.floor(canvasEl.clientWidth * 0.5 * pickerScale);\n const centerY = Math.floor(canvasEl.clientHeight * 0.5 * pickerScale);\n\n const worldPoint = await picker.getWorldPointAsync(centerX, centerY);\n if (worldPoint) {\n const cameraPos = camera.getPosition();\n const distance = cameraPos.distance(worldPoint);\n if (distance > 0.5 && distance < 500) {\n cameraControls.syncFromCamera(worldPoint);\n console.log('[StorySplat Viewer] Updated focus target at distance:', distance.toFixed(2));\n }\n }\n }\n } catch (err) {\n // Picking failed, keep default\n }\n };\n findBetterFocusPoint();\n\n // On mobile, show joystick and camera mode toggle, start in fly mode\n // On desktop, start in orbit mode (left-click drag to orbit, right-click for fly)\n if (isMobile) {\n setJoystickVisible(uiElements, true);\n setCameraModeToggleVisible(uiElements, true);\n // Start in fly mode so joystick works immediately\n cameraControls.setMode('fly');\n updateCameraModeToggle(uiElements, 'fly');\n } else {\n // Desktop starts in orbit mode - user can switch to fly by right-clicking\n cameraControls.setMode('orbit');\n }\n\n // Show return to waypoint button in explore mode (if we have waypoints)\n if (config.waypoints && config.waypoints.length > 0) {\n setReturnWaypointButtonVisible(uiElements, true);\n }\n } else {\n // Tour mode - disable camera controls, enable waypoint control\n cameraControls.disable();\n if (characterController) {\n characterController.disable();\n }\n waypointControlEnabled = true;\n // Hide joystick, camera mode toggle, and return button in tour mode\n setJoystickVisible(uiElements, false);\n setCameraModeToggleVisible(uiElements, false);\n setReturnWaypointButtonVisible(uiElements, false);\n }\n\n events.emit('modeChange', { mode });\n }\n\n // Update preloader progress helper (defined early so loadSplat can use it)\n const updateProgress = (progress: number, text?: string) => {\n if (uiElements.preloader) {\n updatePreloaderProgress(uiElements.preloader, progress, text);\n }\n events.emit('progress', { progress, text });\n };\n\n // Load splat\n async function loadSplat(): Promise<void> {\n // Build URL list with priority: LOD streaming > SOG > PLY/Other formats\n // This ensures backward compatibility with all formats while preferring optimal ones\n const urls: string[] = [];\n\n // Highest priority: LOD streaming format (lod-meta.json) if available\n if (config.lodMetaUrl) urls.push(config.lodMetaUrl);\n\n // Second priority: SOG compressed format\n if (config.sogUrl) urls.push(config.sogUrl);\n\n // Third priority: Original format (PLY, SPLAT, etc.)\n if (config.splatUrl) urls.push(config.splatUrl);\n\n // Fallback URLs for additional format support\n if (config.fallbackUrls) urls.push(...config.fallbackUrls);\n\n console.log('[StorySplat Viewer] Loading splat from URLs:', urls);\n console.log('[StorySplat Viewer] URL priorities: LOD streaming > SOG > PLY/Other');\n updateProgress(0.3, 'Loading splat...');\n\n for (const url of urls) {\n if (!url) continue;\n\n try {\n console.log('[StorySplat Viewer] Trying URL:', url);\n\n // Determine asset type based on extension\n const ext = url.split('.').pop()?.toLowerCase() || 'splat';\n const assetType = 'gsplat'; // PlayCanvas uses 'gsplat' for all gaussian splat formats\n\n const asset = new pc.Asset('splat-' + Date.now(), assetType, { url });\n\n // Track asset loading progress\n asset.on('progress', (received: number, total: number) => {\n if (total > 0) {\n // Map progress from 0.3 to 0.9 (leaving 0.9-1.0 for post-load setup)\n const loadProgress = 0.3 + (received / total) * 0.6;\n updateProgress(loadProgress, `Loading... ${Math.round((received / total) * 100)}%`);\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n let hasRejected = false;\n\n // Only listen for SOG parse errors when loading lod-meta.json (SOG LOD streaming format)\n const isLodFormat = url.includes('lod-meta.json');\n\n // Catch unhandled promise rejections during loading (e.g., SOG v1 parse errors)\n // Only for LOD streaming format which uses the SOG parser\n const unhandledRejectionHandler = isLodFormat ? (event: PromiseRejectionEvent) => {\n if (hasRejected) return;\n const errorMsg = event.reason?.message || String(event.reason);\n // Check if this is a SOG parsing error (deprecated v1 format)\n if (errorMsg.includes('shape') || errorMsg.includes('upgradeMeta')) {\n console.warn('[StorySplat Viewer] Caught SOG parse error, will try fallback:', errorMsg);\n hasRejected = true;\n event.preventDefault(); // Prevent console error\n\n // Emit a deprecation warning event for UI to display\n events.emit('warning', {\n type: 'deprecated_format',\n message: 'This scene uses an outdated SOG format. Please re-upload your scene with the latest tools for better performance.',\n details: 'SOG v1 format is deprecated. Use splat-transform v2+ to convert your PLY files.',\n url: url\n });\n\n reject(new Error(`SOG parse error: ${errorMsg}`));\n }\n } : null;\n\n if (unhandledRejectionHandler) {\n window.addEventListener('unhandledrejection', unhandledRejectionHandler);\n }\n\n // Cleanup handler when done\n const cleanup = () => {\n if (unhandledRejectionHandler) {\n window.removeEventListener('unhandledrejection', unhandledRejectionHandler);\n }\n };\n\n // Use asset.ready() like HTML export - this waits for asset AND resources to be ready\n asset.ready(() => {\n // Check if viewer was destroyed while loading\n if (isDestroyed) {\n console.log('[StorySplat Viewer] Ignoring splat load - viewer was destroyed');\n cleanup();\n reject(new Error('Viewer destroyed'));\n return;\n }\n console.log('[StorySplat Viewer] Asset ready (loaded + resources ready)');\n\n try {\n splatEntity = new pc.Entity('splat');\n splatEntity.addComponent('gsplat', {\n asset: asset,\n unified: true // Enable unified sorting with other transparent objects (hotspots)\n });\n\n // Set LOD distances for LOD streaming format\n // This enables distance-based quality switching when using lod-meta.json\n const gs = splatEntity.gsplat as any;\n if (gs && isLodStreamingFormat(url)) {\n gs.lodDistances = lodPreset.lodDistances;\n console.log('[StorySplat Viewer] LOD distances set for streaming format:', lodPreset.lodDistances);\n }\n\n // Apply scale - match BabylonJS behavior exactly\n // In BabylonJS: invertYScale negates Y, and when invertX !== invertY, Z is also negated to maintain handedness\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n const invertX = config.invertXScale || false;\n const invertY = config.invertYScale || false;\n const finalScale = {\n x: invertX ? -scale.x : scale.x,\n y: invertY ? -scale.y : scale.y,\n z: (invertX !== invertY) ? -scale.z : scale.z // Invert Z when only one of X or Y is inverted\n };\n splatEntity.setLocalScale(finalScale.x, finalScale.y, finalScale.z);\n\n // Apply position (negate Z for Babylon->PlayCanvas coordinate conversion)\n const pos = config.position || [0, 0, 0];\n splatEntity.setPosition(pos[0], pos[1], -pos[2]);\n\n // Apply rotation - convert from BabylonJS (left-handed) to PlayCanvas (right-handed)\n const rot = config.rotation || [0, 0, 0];\n const hasUserRotation = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;\n\n let finalRotDeg: number[];\n if (hasUserRotation) {\n // User specified rotation - apply directly (no base correction)\n // Negate Z for coordinate system conversion\n finalRotDeg = [\n rot[0] * (180 / Math.PI),\n rot[1] * (180 / Math.PI),\n -rot[2] * (180 / Math.PI)\n ];\n } else {\n // No user rotation - apply 180° X base correction for splat orientation\n finalRotDeg = [180, 0, 0];\n }\n\n splatEntity.setEulerAngles(finalRotDeg[0], finalRotDeg[1], finalRotDeg[2]);\n\n console.log('[StorySplat Viewer] Splat transform applied:', {\n scale: finalScale,\n position: [pos[0], pos[1], -pos[2]],\n rotation: finalRotDeg,\n hasUserRotation,\n invertXScale: invertX,\n invertYScale: invertY\n });\n\n app.root.addChild(splatEntity);\n console.log('[StorySplat Viewer] Splat entity added to scene');\n\n // Set alphaClip for GSplat picking support (required for pc.Picker to work with splats)\n // Lower value = more splats are pickable (0.1 is very forgiving)\n if (splatEntity.gsplat?.material) {\n splatEntity.gsplat.material.setParameter('alphaClip', 0.1);\n console.log('[StorySplat Viewer] GSplat alphaClip set for picking support');\n }\n\n // Apply reveal effect if configured (but start disabled - will enable after preloader hides)\n const revealPresetName = (options.revealEffect || scene.revealEffect || 'medium') as RevealPreset;\n const revealConfig = getRevealPreset(revealPresetName);\n\n if (revealConfig) {\n // Add script component for reveal effect\n splatEntity.addComponent('script');\n\n // Create the reveal effect (use getter to get class after app is initialized)\n const GsplatRevealRadialClass = getGsplatRevealRadialClass();\n revealScript = (splatEntity.script as any)?.create(GsplatRevealRadialClass);\n if (revealScript) {\n // Start disabled - will be enabled after preloader hides\n revealScript.enabled = false;\n\n // Apply preset configuration\n revealScript.center.set(0, 0, 0);\n revealScript.speed = revealConfig.speed;\n revealScript.acceleration = revealConfig.acceleration;\n revealScript.delay = revealConfig.delay;\n revealScript.oscillationIntensity = revealConfig.oscillationIntensity;\n revealScript.dotTint.set(revealConfig.dotTint.r, revealConfig.dotTint.g, revealConfig.dotTint.b);\n revealScript.waveTint.set(revealConfig.waveTint.r, revealConfig.waveTint.g, revealConfig.waveTint.b);\n revealScript.endRadius = revealConfig.endRadius;\n\n console.log('[StorySplat Viewer] Reveal effect configured (waiting to start):', revealPresetName);\n }\n } else {\n console.log('[StorySplat Viewer] Reveal effect disabled');\n }\n\n // Wait a brief moment to let async texture loading errors surface\n // before resolving (SOG v1 parse errors happen asynchronously)\n setTimeout(() => {\n if (hasRejected) return; // Already rejected by unhandled rejection handler\n cleanup();\n resolve();\n }, 100);\n } catch (syncError) {\n console.error('[StorySplat Viewer] Error during gsplat setup:', syncError);\n cleanup();\n reject(syncError);\n }\n });\n\n asset.on('error', (err: any) => {\n // Enhanced error logging to help diagnose SOG/PLY loading issues\n console.error('[StorySplat Viewer] Asset load error:', {\n url,\n assetType,\n error: err,\n message: err?.message || 'Unknown error',\n status: err?.status || err?.statusCode || 'N/A',\n stack: err?.stack\n });\n cleanup();\n reject(err);\n });\n\n app.assets.add(asset);\n app.assets.load(asset);\n });\n\n events.emit('loaded');\n console.log('[StorySplat Viewer] Splat loaded successfully');\n return;\n } catch (err) {\n console.warn('[StorySplat Viewer] Failed to load:', url, err);\n }\n }\n\n console.error('[StorySplat Viewer] Failed to load splat from any URL');\n events.emit('error', new Error('Failed to load splat from any URL'));\n }\n\n // Helper to convert waypoint rotation to PlayCanvas quaternion\n // Matching POC waypointNavigation.ts exactly - NO yFlip\n function convertWaypointRotation(rot: any): pc.Quat {\n if ('_w' in rot || 'w' in rot) {\n // Quaternion format - BabylonJS to PlayCanvas conversion\n // Negate X and Y for handedness conversion (matching POC exactly)\n const qx = rot._x ?? rot.x ?? 0;\n const qy = rot._y ?? rot.y ?? 0;\n const qz = rot._z ?? rot.z ?? 0;\n const qw = rot._w ?? rot.w ?? 1;\n return new pc.Quat(-qx, -qy, qz, qw);\n } else if ('x' in rot && 'y' in rot && 'z' in rot) {\n // Euler angles\n const result = new pc.Quat();\n result.setFromEulerAngles(rot.x || 0, rot.y || 0, rot.z || 0);\n return result;\n } else {\n return new pc.Quat();\n }\n }\n\n // =========================================================\n // SPLATSWAP SYSTEM\n // Handles swapping between multiple splat files during navigation\n // =========================================================\n\n /**\n * Hide a preloaded splat (Option 1: keep in memory)\n */\n function hideSplat(entity: pc.Entity): void {\n entity.enabled = false;\n console.log('[SplatSwap] Splat hidden');\n }\n\n /**\n * Dispose of a preloaded splat (Option 2: free memory)\n */\n function disposeSplat(url: string): void {\n const entity = preloadedSplats.get(url);\n if (entity) {\n entity.destroy();\n preloadedSplats.delete(url);\n console.log('[SplatSwap] Splat disposed:', url);\n }\n }\n\n /**\n * Show a preloaded splat\n */\n function showSplat(url: string): boolean {\n const entity = preloadedSplats.get(url);\n if (!entity) return false;\n\n entity.enabled = true;\n currentSplatUrl = url;\n console.log('[SplatSwap] Splat shown:', url);\n return true;\n }\n\n /**\n * Preload a splat in the background without displaying it\n */\n async function preloadSplat(url: string): Promise<void> {\n if (preloadedSplats.has(url)) return;\n if (url === currentSplatUrl) return;\n if (isDestroyed) return;\n\n console.log('[SplatSwap] Preloading splat:', url);\n\n try {\n const asset = new pc.Asset('splat-preload-' + Date.now(), 'gsplat', { url });\n\n await new Promise<void>((resolve, reject) => {\n asset.ready(() => {\n if (isDestroyed) {\n reject(new Error('Viewer destroyed'));\n return;\n }\n\n const entity = new pc.Entity('splat-preload');\n entity.addComponent('gsplat', { asset, unified: true });\n\n // Apply scale/position/rotation matching main splat\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n const invertX = config.invertXScale || false;\n const invertY = config.invertYScale || false;\n const finalScale = {\n x: invertX ? -scale.x : scale.x,\n y: invertY ? -scale.y : scale.y,\n z: (invertX !== invertY) ? -scale.z : scale.z\n };\n entity.setLocalScale(finalScale.x, finalScale.y, finalScale.z);\n\n const pos = config.position || [0, 0, 0];\n entity.setPosition(pos[0], pos[1], -pos[2]);\n\n const rot = config.rotation || [0, 0, 0];\n const hasUserRotation = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;\n const finalRotDeg = hasUserRotation\n ? [rot[0] * (180 / Math.PI), rot[1] * (180 / Math.PI), -rot[2] * (180 / Math.PI)]\n : [180, 0, 0];\n entity.setEulerAngles(finalRotDeg[0], finalRotDeg[1], finalRotDeg[2]);\n\n // Start hidden\n entity.enabled = false;\n app.root.addChild(entity);\n preloadedSplats.set(url, entity);\n\n console.log('[SplatSwap] Preload complete:', url);\n resolve();\n });\n\n asset.on('error', (err: any) => {\n console.error('[SplatSwap] Preload error:', err);\n reject(err);\n });\n\n app.assets.add(asset);\n app.assets.load(asset);\n });\n } catch (error) {\n console.error('[SplatSwap] Error preloading:', url, error);\n }\n }\n\n /**\n * Preload the next splat in the sequence\n */\n async function preloadNextSplat(currentIndex: number): Promise<void> {\n if (!additionalSplats || additionalSplats.length === 0) return;\n\n const nextIndex = (currentIndex + 1) % additionalSplats.length;\n const nextSplat = additionalSplats[nextIndex];\n if (nextSplat && nextSplat.url) {\n await preloadSplat(nextSplat.url);\n }\n }\n\n /**\n * Load a splat (switch to it, handling show/hide or dispose)\n */\n async function loadSwapSplat(url: string): Promise<void> {\n if (url === currentSplatUrl) return;\n if (isLoadingSplat) return;\n if (isDestroyed) return;\n\n isLoadingSplat = true;\n console.log('[SplatSwap] Switching to splat:', url);\n\n try {\n // Hide/dispose current splat\n if (currentSplatUrl && preloadedSplats.has(currentSplatUrl)) {\n if (keepMeshesInMemory) {\n const currentEntity = preloadedSplats.get(currentSplatUrl);\n if (currentEntity) hideSplat(currentEntity);\n } else {\n disposeSplat(currentSplatUrl);\n }\n } else if (splatEntity) {\n // Hide the initial splat entity\n splatEntity.enabled = false;\n }\n\n // Show new splat if already preloaded\n if (preloadedSplats.has(url)) {\n showSplat(url);\n } else {\n // Load new splat\n const asset = new pc.Asset('splat-swap-' + Date.now(), 'gsplat', { url });\n\n await new Promise<void>((resolve, reject) => {\n asset.ready(() => {\n if (isDestroyed) {\n reject(new Error('Viewer destroyed'));\n return;\n }\n\n const entity = new pc.Entity('splat-swap');\n entity.addComponent('gsplat', { asset, unified: true });\n\n // Apply scale/position/rotation\n const scale = config.scale || { x: 1, y: 1, z: 1 };\n const invertX = config.invertXScale || false;\n const invertY = config.invertYScale || false;\n const finalScale = {\n x: invertX ? -scale.x : scale.x,\n y: invertY ? -scale.y : scale.y,\n z: (invertX !== invertY) ? -scale.z : scale.z\n };\n entity.setLocalScale(finalScale.x, finalScale.y, finalScale.z);\n\n const pos = config.position || [0, 0, 0];\n entity.setPosition(pos[0], pos[1], -pos[2]);\n\n const rot = config.rotation || [0, 0, 0];\n const hasUserRotation = rot[0] !== 0 || rot[1] !== 0 || rot[2] !== 0;\n const finalRotDeg = hasUserRotation\n ? [rot[0] * (180 / Math.PI), rot[1] * (180 / Math.PI), -rot[2] * (180 / Math.PI)]\n : [180, 0, 0];\n entity.setEulerAngles(finalRotDeg[0], finalRotDeg[1], finalRotDeg[2]);\n\n app.root.addChild(entity);\n preloadedSplats.set(url, entity);\n currentSplatUrl = url;\n\n console.log('[SplatSwap] New splat loaded and shown:', url);\n resolve();\n });\n\n asset.on('error', (err: any) => {\n console.error('[SplatSwap] Load error:', err);\n reject(err);\n });\n\n app.assets.add(asset);\n app.assets.load(asset);\n });\n }\n\n // Background preload next splat\n const swapIndex = additionalSplats.findIndex(s => s.url === url);\n if (swapIndex !== -1) {\n preloadNextSplat(swapIndex);\n }\n\n } catch (error) {\n console.error('[SplatSwap] Error switching splat:', error);\n } finally {\n isLoadingSplat = false;\n initialSplatLoadDone = true;\n }\n }\n\n /**\n * Get the primary splat URL (first URL tried in loadSplat)\n */\n function getPrimarySplatUrl(): string {\n if (config.sogUrl) return config.sogUrl;\n if (config.splatUrl) return config.splatUrl;\n if (config.fallbackUrls && config.fallbackUrls.length > 0) return config.fallbackUrls[0];\n return '';\n }\n\n /**\n * Update splats based on current progress (called from navigation loop)\n */\n function updateSplats(): void {\n if (!additionalSplats || additionalSplats.length === 0) return;\n\n const numWaypoints = config.waypoints?.length || 1;\n const percentage = currentProgress * 100;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n\n // Skip if no meaningful change\n if (Math.abs(percentage - lastSplatCheckProgress) < 0.1 &&\n waypointIndex === lastSplatCheckWaypointIndex) {\n return;\n }\n\n lastSplatCheckProgress = percentage;\n lastSplatCheckWaypointIndex = waypointIndex;\n\n // Find the best matching splat based on progress\n // Use separate tracking for waypoint and percentage triggers to avoid\n // comparing different value ranges (waypoint 0-N vs percentage 0-100)\n type SplatSwapType = (typeof additionalSplats)[number];\n let bestWaypointSplat: SplatSwapType | null = null;\n let bestPercentageSplat: SplatSwapType | null = null;\n let bestWaypointTrigger = -Infinity;\n let bestPercentageTrigger = -Infinity;\n\n for (const splat of additionalSplats) {\n if (splat.waypointIndex !== -1) {\n // Trigger by waypoint index\n if (waypointIndex >= splat.waypointIndex && splat.waypointIndex > bestWaypointTrigger) {\n bestWaypointTrigger = splat.waypointIndex;\n bestWaypointSplat = splat;\n }\n } else if (splat.percentage !== -1) {\n // Trigger by percentage\n if (percentage >= splat.percentage && splat.percentage > bestPercentageTrigger) {\n bestPercentageTrigger = splat.percentage;\n bestPercentageSplat = splat;\n }\n }\n }\n\n // Prefer percentage-based triggers over waypoint-based (percentage is more precise)\n const bestSplat = bestPercentageSplat || bestWaypointSplat;\n\n // Determine target URL\n const primaryUrl = getPrimarySplatUrl();\n const targetUrl = bestSplat ? bestSplat.url : primaryUrl;\n\n // Switch splat if needed\n if (targetUrl && targetUrl !== currentSplatUrl) {\n if (targetUrl === primaryUrl && splatEntity && !currentSplatUrl) {\n // First load - just mark current URL\n currentSplatUrl = primaryUrl;\n } else if (targetUrl === primaryUrl && splatEntity) {\n // Return to primary splat\n // Hide swap splats and show primary\n preloadedSplats.forEach((entity, url) => {\n if (url !== primaryUrl) {\n if (keepMeshesInMemory) {\n hideSplat(entity);\n } else {\n disposeSplat(url);\n }\n }\n });\n if (splatEntity) splatEntity.enabled = true;\n currentSplatUrl = primaryUrl;\n console.log('[SplatSwap] Returned to primary splat');\n } else {\n // Switch to a different splat\n loadSwapSplat(targetUrl);\n }\n\n // Apply per-splat skybox if specified\n if (bestSplat && bestSplat.skyboxUrl) {\n applySkybox(bestSplat.skyboxUrl, bestSplat.skyboxRotation || 0);\n }\n }\n }\n\n /**\n * Apply a skybox (for per-splat skyboxes)\n * Note: For proper cubemap skyboxes, use 6-face URLs or HDR/EXR environment maps\n * This simplified version loads a single texture and creates a basic skybox\n */\n function applySkybox(url: string, rotation: number = 0): void {\n console.log('[SplatSwap] Applying skybox:', url, 'rotation:', rotation);\n\n // Load skybox as a cubemap texture\n const skyboxAsset = new pc.Asset('skybox-swap-' + Date.now(), 'cubemap', {\n url: url\n }, {\n // Try to load as RGBM for HDR support\n type: pc.TEXTURETYPE_RGBM,\n mipmaps: true\n });\n\n skyboxAsset.ready(() => {\n if (isDestroyed) return;\n\n try {\n // Set as scene skybox - cast to Texture to satisfy TypeScript\n app.scene.skybox = skyboxAsset.resource as pc.Texture;\n app.scene.skyboxMip = 0; // Use highest quality mip\n\n // Apply rotation via quaternion (convert radians to Euler then to Quat)\n if (rotation !== 0) {\n const rotQuat = new pc.Quat();\n rotQuat.setFromEulerAngles(0, rotation * (180 / Math.PI), 0);\n app.scene.skyboxRotation = rotQuat;\n }\n\n console.log('[SplatSwap] Skybox applied successfully');\n } catch (error) {\n console.error('[SplatSwap] Error applying skybox:', error);\n }\n });\n\n skyboxAsset.on('error', (err: any) => {\n console.error('[SplatSwap] Skybox load error:', err);\n });\n\n app.assets.add(skyboxAsset);\n app.assets.load(skyboxAsset);\n }\n\n // =========================================================\n // END SPLATSWAP SYSTEM\n // =========================================================\n\n // Progress-based navigation (matching HTML export)\n let currentProgress = 0; // 0 to 1\n const totalDuration = config.waypoints?.reduce((sum, wp) => sum + (wp.duration || 2000), 0) || 1;\n // Use config.autoplaySpeed if provided, otherwise calculate from total duration\n const playbackSpeed = config.autoplaySpeed !== undefined ? config.autoplaySpeed : 1000 / totalDuration;\n // Normalize loopMode: handle boolean values (true='loop', false='none') and string values\n // Scene data may have boolean loopMode from older versions\n const rawLoopMode = config.loopMode as unknown;\n let loopMode: 'loop' | 'pingpong' | 'none';\n if (rawLoopMode === true) {\n loopMode = 'loop';\n } else if (rawLoopMode === false) {\n loopMode = 'none';\n } else if (rawLoopMode === 'loop' || rawLoopMode === 'pingpong' || rawLoopMode === 'none') {\n loopMode = rawLoopMode;\n } else {\n loopMode = 'loop'; // Default to 'loop' for backward compatibility\n }\n let playbackDirection = 1; // 1 = forward, -1 = backward (for pingpong mode)\n\n console.log('[StorySplat Viewer] Playback config:', {\n loopMode,\n playbackSpeed,\n totalDuration,\n autoPlay: config.autoPlay,\n rawLoopMode: config.loopMode\n });\n\n // Damped camera interpolation (matching Babylon's pullStrength behavior)\n let targetCameraPosition: pc.Vec3 | null = null;\n let targetCameraRotation: pc.Quat | null = null;\n const PULL_STRENGTH = 0.1; // Match Babylon's pullStrength = 0.1\n\n // Elastic user rotation (allows user to look around in tour mode, pulls back to target)\n // Using Euler angles (yaw/pitch) instead of quaternion accumulation to prevent roll drift\n let userYawOffset = 0; // Accumulated yaw offset in degrees\n let userPitchOffset = 0; // Accumulated pitch offset in degrees\n const MAX_PITCH_OFFSET = 60; // Limit pitch to prevent flipping\n let isUserDragging = false;\n let lastPointerX = 0;\n let lastPointerY = 0;\n const USER_ROTATION_SENSITIVITY = 0.3; // How responsive rotation is to drag\n const USER_ROTATION_DECAY = 0.95; // How fast rotation returns to target (0.95 = slow, 0.8 = fast)\n\n // Pre-calculate waypoint positions, rotations, and FOVs for interpolation\n const waypointPositions: pc.Vec3[] = [];\n const waypointRotations: pc.Quat[] = [];\n const waypointFOVs: number[] = [];\n const defaultFOV = config.fov || 60;\n let targetFOV = defaultFOV;\n\n if (config.waypoints && config.waypoints.length > 0) {\n config.waypoints.forEach(wp => {\n const pos = wp.position\n ? new pc.Vec3(wp.position.x, wp.position.y, -wp.position.z)\n : new pc.Vec3(0, config.playerHeight || 1.6, 0);\n waypointPositions.push(pos);\n\n const rot = wp.rotation\n ? convertWaypointRotation(wp.rotation)\n : new pc.Quat();\n waypointRotations.push(rot);\n\n // Extract FOV from waypoint (convert from radians if needed)\n let fov = wp.fov || defaultFOV;\n if (fov < 3.5) {\n // FOV is in radians, convert to degrees\n fov = fov * (180 / Math.PI);\n }\n waypointFOVs.push(fov);\n });\n\n // Initialize target position/rotation/FOV immediately for damping to work on load\n if (waypointPositions.length > 0) {\n targetCameraPosition = waypointPositions[0].clone();\n targetCameraRotation = waypointRotations[0].clone();\n targetFOV = waypointFOVs[0];\n }\n }\n\n // Update camera based on progress (sets targets for damped interpolation)\n function updateCameraFromProgress(progress: number): void {\n if (!waypointControlEnabled || waypointPositions.length < 2) return;\n\n progress = Math.max(0, Math.min(1, progress));\n const numWaypoints = waypointPositions.length;\n\n // Calculate which segment we're in\n const segmentProgress = progress * (numWaypoints - 1);\n const segmentIndex = Math.min(Math.floor(segmentProgress), numWaypoints - 2);\n const t = segmentProgress - segmentIndex;\n\n // Calculate target position (for damped interpolation)\n const startPos = waypointPositions[segmentIndex];\n const endPos = waypointPositions[segmentIndex + 1];\n targetCameraPosition = new pc.Vec3(\n lerp(startPos.x, endPos.x, t),\n lerp(startPos.y, endPos.y, t),\n lerp(startPos.z, endPos.z, t)\n );\n\n // Calculate target rotation (for damped interpolation)\n const startRot = waypointRotations[segmentIndex];\n const endRot = waypointRotations[segmentIndex + 1];\n targetCameraRotation = new pc.Quat();\n targetCameraRotation.slerp(startRot, endRot, t);\n\n // Calculate target FOV (for damped interpolation)\n const startFOV = waypointFOVs[segmentIndex];\n const endFOV = waypointFOVs[segmentIndex + 1];\n targetFOV = lerp(startFOV, endFOV, t);\n\n // Update waypoint index and emit event if changed\n const newIndex = Math.round(progress * (numWaypoints - 1));\n if (newIndex !== currentWaypointIndex) {\n const prevIndex = currentWaypointIndex;\n currentWaypointIndex = newIndex;\n\n // Get current waypoint's camera mode\n const currentWaypoint = config.waypoints![newIndex];\n const waypointCameraMode = currentWaypoint.cameraMode || 'first-person';\n\n // Update orbit mode state based on per-waypoint camera mode\n if (waypointCameraMode === 'orbit') {\n // Enable orbit-style controls while on tour path\n currentWaypointOrbitEnabled = true;\n // Set orbit target (waypoint position or custom target)\n currentWaypointOrbitTarget = currentWaypoint.orbitTarget\n ? new pc.Vec3(\n currentWaypoint.orbitTarget.x,\n currentWaypoint.orbitTarget.y,\n -(currentWaypoint.orbitTarget.z || 0) // Negate Z for coordinate conversion\n )\n : new pc.Vec3(\n waypointPositions[newIndex].x,\n waypointPositions[newIndex].y,\n waypointPositions[newIndex].z\n );\n } else {\n currentWaypointOrbitEnabled = false;\n currentWaypointOrbitTarget = null;\n }\n\n events.emit('waypointChange', {\n index: newIndex,\n waypoint: currentWaypoint,\n prevIndex,\n cameraMode: waypointCameraMode\n });\n\n // Update waypoint audio (play/stop based on waypoint entry/exit)\n updateWaypointAudio(newIndex, prevIndex);\n }\n\n // Emit progress event for UI (ensure clamped value)\n events.emit('progressUpdate', { progress: Math.max(0, Math.min(1, progress)), index: currentWaypointIndex });\n\n // Update splat swap system based on progress\n updateSplats();\n }\n\n // Apply damped camera interpolation (matching Babylon's pullStrength behavior)\n // Only active in tour mode - explore/walk modes control the camera directly\n function updateCameraDamping(): void {\n if (!targetCameraPosition || !targetCameraRotation) return;\n if (!waypointControlEnabled) return; // Don't interfere with explore/walk mode cameras\n\n // Lerp position toward target (matching Babylon's 0.1 pullStrength)\n const currentPos = camera.getPosition();\n const newPos = new pc.Vec3();\n newPos.lerp(currentPos, targetCameraPosition, PULL_STRENGTH);\n camera.setPosition(newPos.x, newPos.y, newPos.z);\n\n // Lerp FOV toward target for smooth transitions between waypoints\n const cameraComponent = camera.camera;\n if (cameraComponent && waypointFOVs.length > 0) {\n const currentFOV = cameraComponent.fov;\n const newFOV = lerp(currentFOV, targetFOV, PULL_STRENGTH);\n cameraComponent.fov = newFOV;\n }\n\n // Decay user rotation offset when not dragging (elastic pull back to target)\n if (!isUserDragging) {\n userYawOffset *= USER_ROTATION_DECAY;\n userPitchOffset *= USER_ROTATION_DECAY;\n // Snap to zero if very small to avoid floating point drift\n if (Math.abs(userYawOffset) < 0.01) userYawOffset = 0;\n if (Math.abs(userPitchOffset) < 0.01) userPitchOffset = 0;\n }\n\n // Build user rotation quaternion from Euler angles (no roll)\n const userRotationOffset = new pc.Quat();\n userRotationOffset.setFromEulerAngles(userPitchOffset, userYawOffset, 0);\n\n // Combine target rotation with user offset for final rotation target\n const targetWithUserOffset = new pc.Quat();\n targetWithUserOffset.mul2(targetCameraRotation, userRotationOffset);\n\n // Slerp rotation toward combined target\n const currentRot = camera.getRotation();\n const newRot = new pc.Quat();\n newRot.slerp(currentRot, targetWithUserOffset, PULL_STRENGTH);\n camera.setRotation(newRot);\n }\n\n // Register camera damping in the render loop\n app.on('update', updateCameraDamping);\n\n // Set progress directly (for scroll)\n function setProgress(progress: number, animate = false): void {\n if (animate) {\n animateToProgress(progress);\n } else {\n currentProgress = Math.max(0, Math.min(1, progress));\n updateCameraFromProgress(currentProgress);\n }\n }\n\n // Base transition duration (ms) - can be scaled by transitionSpeed setting\n const baseTransitionDuration = 500;\n // transitionSpeed is a multiplier (e.g., 0.5 = faster, 2 = slower)\n const transitionDuration = baseTransitionDuration / (config.transitionSpeed || 1);\n\n // Animate to a target progress\n function animateToProgress(targetProgress: number, duration = transitionDuration): void {\n const startProgress = currentProgress;\n const startTime = performance.now();\n\n const animate = () => {\n const elapsed = performance.now() - startTime;\n const t = Math.min(elapsed / duration, 1);\n const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n\n currentProgress = startProgress + (targetProgress - startProgress) * eased;\n updateCameraFromProgress(currentProgress);\n\n if (t < 1) {\n requestAnimationFrame(animate);\n }\n };\n\n requestAnimationFrame(animate);\n }\n\n // Navigation functions\n function goToWaypoint(index: number): void {\n if (!config.waypoints || index < 0 || index >= config.waypoints.length) return;\n if (!waypointControlEnabled) return;\n\n const targetProgress = index / Math.max(1, config.waypoints.length - 1);\n animateToProgress(targetProgress);\n }\n\n function nextWaypoint(): void {\n if (!config.waypoints || config.waypoints.length === 0) return;\n\n // scrollButtonMode: 'waypoint' jumps to next waypoint, 'percentage' scrolls by scrollAmount\n const buttonMode = config.scrollButtonMode || 'waypoint';\n if (buttonMode === 'percentage' || buttonMode === 'continuous' || buttonMode === 'incremental') {\n // Percentage mode: scroll by scrollAmount percentage\n const scrollAmountSetting = config.scrollAmount || 10;\n const increment = scrollAmountSetting / 100; // Convert percentage to 0-1\n const newProgress = Math.min(1, currentProgress + increment);\n animateToProgress(newProgress);\n } else {\n // Waypoint mode: jump to next waypoint\n const next = Math.min(currentWaypointIndex + 1, config.waypoints.length - 1);\n goToWaypoint(next);\n }\n }\n\n function prevWaypoint(): void {\n if (!config.waypoints || config.waypoints.length === 0) return;\n\n // scrollButtonMode: 'waypoint' jumps to prev waypoint, 'percentage' scrolls by scrollAmount\n const buttonMode = config.scrollButtonMode || 'waypoint';\n if (buttonMode === 'percentage' || buttonMode === 'continuous' || buttonMode === 'incremental') {\n // Percentage mode: scroll by scrollAmount percentage\n const scrollAmountSetting = config.scrollAmount || 10;\n const increment = scrollAmountSetting / 100; // Convert percentage to 0-1\n const newProgress = Math.max(0, currentProgress - increment);\n animateToProgress(newProgress);\n } else {\n // Waypoint mode: jump to prev waypoint\n const prev = Math.max(currentWaypointIndex - 1, 0);\n goToWaypoint(prev);\n }\n }\n\n // Continuous playback using requestAnimationFrame\n let lastPlaybackTime = 0;\n let playbackAnimationId: number | null = null;\n\n function playbackLoop(timestamp: number): void {\n if (!isPlaying) return;\n\n if (lastPlaybackTime === 0) lastPlaybackTime = timestamp;\n const deltaTime = (timestamp - lastPlaybackTime) / 1000;\n lastPlaybackTime = timestamp;\n\n // Increment/decrement progress based on direction\n currentProgress += playbackSpeed * deltaTime * playbackDirection;\n\n // Handle end of tour based on loop mode\n if (currentProgress >= 1) {\n console.log('[StorySplat Viewer] End of tour reached, loopMode:', loopMode);\n switch (loopMode) {\n case 'loop':\n console.log('[StorySplat Viewer] Looping - restarting at beginning');\n currentProgress = 0; // Restart at beginning\n break;\n case 'pingpong':\n console.log('[StorySplat Viewer] Pingpong - reversing direction');\n currentProgress = 1;\n playbackDirection = -1; // Reverse direction\n break;\n case 'none':\n console.log('[StorySplat Viewer] No loop - stopping playback');\n currentProgress = 1;\n pause(); // Stop playback\n events.emit('playbackComplete');\n return;\n default:\n console.log('[StorySplat Viewer] Unknown loopMode, stopping:', loopMode);\n currentProgress = 1;\n pause(); // Stop playback\n events.emit('playbackComplete');\n return;\n }\n } else if (currentProgress <= 0) {\n // Only relevant for pingpong mode going backward\n switch (loopMode) {\n case 'pingpong':\n console.log('[StorySplat Viewer] Pingpong - reversing to forward');\n currentProgress = 0;\n playbackDirection = 1; // Reverse back to forward\n break;\n default:\n currentProgress = 0;\n break;\n }\n }\n\n updateCameraFromProgress(currentProgress);\n playbackAnimationId = requestAnimationFrame(playbackLoop);\n }\n\n function play(): void {\n if (isPlaying || !config.waypoints || config.waypoints.length < 2) return;\n isPlaying = true;\n lastPlaybackTime = 0;\n playbackDirection = 1; // Always start going forward\n events.emit('playbackStart');\n playbackAnimationId = requestAnimationFrame(playbackLoop);\n }\n\n function pause(): void {\n isPlaying = false;\n if (playbackAnimationId) {\n cancelAnimationFrame(playbackAnimationId);\n playbackAnimationId = null;\n }\n events.emit('playbackStop');\n }\n\n function stop(): void {\n pause();\n setProgress(0);\n }\n\n // Scroll wheel support\n const scrollCanvas = app.graphicsDevice.canvas as HTMLCanvasElement;\n scrollCanvas.addEventListener('wheel', (e: WheelEvent) => {\n if (!waypointControlEnabled) return;\n e.preventDefault();\n\n // Adjust progress based on scroll using configurable speed and amount\n // scrollSpeed is 0.01-0.5, scrollAmount is 0-100 (percentage of tour per scroll)\n const scrollSpeedSetting = config.scrollSpeed || 0.1;\n const scrollAmountSetting = config.scrollAmount || 100;\n // Base increment scaled by settings: scrollAmount/100 gives 0-1 fraction, scrollSpeed multiplies it\n const baseIncrement = (scrollAmountSetting / 100) * scrollSpeedSetting * 0.05; // 0.05 base for smooth scroll\n const scrollIncrement = e.deltaY > 0 ? baseIncrement : -baseIncrement;\n const newProgress = Math.max(0, Math.min(1, currentProgress + scrollIncrement));\n setProgress(newProgress);\n }, { passive: false });\n\n // Elastic user rotation - track pointer drag for look-around in tour mode\n // Use capture phase to get events before PlayCanvas input system\n scrollCanvas.addEventListener('pointerdown', (e: PointerEvent) => {\n if (!waypointControlEnabled) return; // Only in tour/waypoint mode\n isUserDragging = true;\n lastPointerX = e.clientX;\n lastPointerY = e.clientY;\n }, { capture: true });\n\n scrollCanvas.addEventListener('pointermove', (e: PointerEvent) => {\n if (!waypointControlEnabled || !isUserDragging) return;\n\n const deltaX = e.clientX - lastPointerX;\n const deltaY = e.clientY - lastPointerY;\n lastPointerX = e.clientX;\n lastPointerY = e.clientY;\n\n // Convert drag delta to rotation offset in degrees (yaw and pitch only, no roll)\n const yawDelta = -deltaX * USER_ROTATION_SENSITIVITY;\n const pitchDelta = -deltaY * USER_ROTATION_SENSITIVITY;\n\n // Accumulate yaw and pitch offsets (Euler angles prevent roll drift)\n userYawOffset += yawDelta;\n userPitchOffset += pitchDelta;\n\n // Clamp pitch to prevent camera flipping\n userPitchOffset = Math.max(-MAX_PITCH_OFFSET, Math.min(MAX_PITCH_OFFSET, userPitchOffset));\n }, { capture: true });\n\n scrollCanvas.addEventListener('pointerup', () => {\n isUserDragging = false;\n }, { capture: true });\n\n scrollCanvas.addEventListener('pointerleave', () => {\n isUserDragging = false;\n }, { capture: true });\n\n // =====================================================\n // HOTSPOT SYSTEM\n // =====================================================\n const hotspotEntities: pc.Entity[] = [];\n\n // =====================================================\n // PORTAL SYSTEM (Scene-to-Scene Navigation)\n // =====================================================\n const portalEntities: pc.Entity[] = [];\n\n // Parse hex color to PlayCanvas Color\n function parseColor(hex: string): pc.Color {\n const r = parseInt(hex.slice(1, 3), 16) / 255;\n const g = parseInt(hex.slice(3, 5), 16) / 255;\n const b = parseInt(hex.slice(5, 7), 16) / 255;\n return new pc.Color(r, g, b);\n }\n\n // =====================================================\n // SPATIAL AUDIO SYSTEM\n // =====================================================\n interface HotspotAudioElements {\n audio: HTMLAudioElement;\n audioCtx?: AudioContext;\n source?: MediaElementAudioSourceNode;\n panner?: PannerNode;\n updateAudioPosition?: () => void;\n }\n\n interface VideoSpatialAudio {\n audioCtx: AudioContext;\n source: MediaElementAudioSourceNode;\n panner: PannerNode;\n }\n\n interface WaypointAudioData {\n entity: pc.Entity;\n waypointIndex: number;\n config: any;\n slotId: string;\n playing: boolean;\n autoplayTriggered: boolean;\n assetReady: boolean;\n }\n\n // Track all audio for muting\n const allAudioContexts: AudioContext[] = [];\n const allAudioElements: HTMLAudioElement[] = [];\n let globalMuted = false;\n const storedVolumes = new Map<HTMLAudioElement, number>();\n\n // Waypoint audio tracking\n const waypointAudioMap = new Map<string, WaypointAudioData>();\n\n // =====================================================\n // PARTICLE SYSTEM\n // =====================================================\n const particleEntities: Map<string, pc.Entity> = new Map();\n const particleTextures: Map<string, pc.Texture> = new Map();\n\n // Particle texture URLs (matching HTML export particleSystem.ts)\n const PARTICLE_TEXTURE_URLS: Record<string, string> = {\n flare: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fflare.png?alt=media&token=ce114781-2ac3-41b2-b9c2-34bda0f6eb13',\n circle: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fcircle.png?alt=media&token=fd01b475-2b94-4c24-bc83-2a907045715f',\n spark: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fsparkle.png?alt=media&token=466739a4-ccd7-4295-88c2-dedd681a34f0',\n rain: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Frain.png?alt=media&token=13478487-6259-4906-838c-ad592db893e4',\n smoke: 'https://firebasestorage.googleapis.com/v0/b/story-splat.firebasestorage.app/o/public%2Fparticles%2Fsmoke.png?alt=media&token=6cece4f8-87cf-47f9-974d-c4dd6a649f0f',\n };\n\n /**\n * Load a particle texture (with caching)\n */\n function loadParticleTexture(name: string, url: string): Promise<pc.Texture> {\n return new Promise((resolve, reject) => {\n // Check cache first\n if (particleTextures.has(name)) {\n resolve(particleTextures.get(name)!);\n return;\n }\n\n const asset = new pc.Asset(name, 'texture', { url: url });\n asset.on('load', () => {\n // Check if viewer was destroyed while loading\n if (isDestroyed) {\n console.log(`[Particle] Ignoring texture load - viewer was destroyed: ${name}`);\n reject(new Error('Viewer destroyed'));\n return;\n }\n const texture = asset.resource as pc.Texture;\n particleTextures.set(name, texture);\n resolve(texture);\n });\n asset.on('error', (err: any) => {\n console.error(`[Particle] Failed to load texture: ${name}`, err);\n reject(err);\n });\n app.assets.add(asset);\n app.assets.load(asset);\n });\n }\n\n /**\n * Create a particle system entity (matching HTML export createParticleSystem)\n * Enhanced with full feature parity: direction1/direction2, box emitter positioning,\n * scaleX/Y, colorDead, angular speed range, initial rotation range\n */\n function createParticleSystemEntity(psConfig: any): pc.Entity {\n const entity = new pc.Entity(psConfig.name || 'particle-system');\n\n // Helper to safely get position coordinates\n const getPos = (vec: any, defaultVal = 0) => ({\n x: vec?._x ?? vec?.x ?? defaultVal,\n y: vec?._y ?? vec?.y ?? defaultVal,\n z: vec?._z ?? vec?.z ?? defaultVal\n });\n\n // Helper to safely get color\n const getColor = (color: any) => color || { r: 1, g: 1, b: 1, a: 1 };\n\n const emitterPos = getPos(psConfig.emitterPosition, 0);\n const gravity = getPos(psConfig.gravity, 0);\n const color1 = getColor(psConfig.color1);\n const color2 = getColor(psConfig.color2);\n const colorDead = getColor(psConfig.colorDead);\n\n // Get direction1/direction2 for directional emission (matching HTML export)\n const direction1 = getPos(psConfig.direction1, 0);\n const direction2 = getPos(psConfig.direction2, 0);\n\n // Calculate lifetime from min/max\n const minLifeTime = psConfig.minLifeTime ?? 1.0;\n const maxLifeTime = psConfig.maxLifeTime ?? 3.0;\n const lifetime = (minLifeTime + maxLifeTime) / 2;\n\n // Determine emitter shape and calculate extents\n let emitterShape: number = pc.EMITTERSHAPE_BOX;\n let emitterExtents = new pc.Vec3(0.1, 0.1, 0.1);\n let emitterOffset = new pc.Vec3(0, 0, 0);\n\n if (psConfig.emitterType === 'sphere' || psConfig.emitterShape === 'sphere') {\n emitterShape = pc.EMITTERSHAPE_SPHERE;\n const radius = psConfig.emitterRadius || 0.1;\n emitterExtents = new pc.Vec3(radius, radius, radius);\n } else if (psConfig.emitterShape === 'box' && psConfig.emitBoxMin && psConfig.emitBoxMax) {\n // Box emitter with min/max emit box (matching HTML export)\n emitterShape = pc.EMITTERSHAPE_BOX;\n const boxMin = getPos(psConfig.emitBoxMin, 0);\n const boxMax = getPos(psConfig.emitBoxMax, 0);\n\n // Calculate extents as half the size of the box\n emitterExtents = new pc.Vec3(\n Math.abs(boxMax.x - boxMin.x) / 2,\n Math.abs(boxMax.y - boxMin.y) / 2,\n Math.abs(boxMax.z - boxMin.z) / 2\n );\n\n // Calculate center offset from emitter position\n emitterOffset = new pc.Vec3(\n (boxMin.x + boxMax.x) / 2,\n (boxMin.y + boxMax.y) / 2,\n -(boxMin.z + boxMax.z) / 2 // Negate Z for coordinate conversion\n );\n } else if (psConfig.emitterShape === 'point') {\n // Point emitter - use very small extents\n emitterShape = pc.EMITTERSHAPE_BOX;\n emitterExtents = new pc.Vec3(0.01, 0.01, 0.01);\n } else {\n // Default box emitter\n emitterExtents = new pc.Vec3(\n psConfig.emitterExtents?.x || psConfig.emitterRadius || 0.1,\n psConfig.emitterExtents?.y || psConfig.emitterRadius || 0.1,\n psConfig.emitterExtents?.z || psConfig.emitterRadius || 0.1\n );\n }\n\n // Determine blend mode (matching HTML export blendModeMap)\n let blendMode: number = pc.BLEND_ADDITIVEALPHA;\n const blendModeStr = psConfig.blendMode || '';\n if (blendModeStr === 'BLENDMODE_STANDARD' || blendModeStr === 'normal' || blendModeStr === 'alpha') {\n blendMode = pc.BLEND_NORMAL;\n } else if (blendModeStr === 'BLENDMODE_MULTIPLY' || blendModeStr === 'multiply') {\n blendMode = pc.BLEND_MULTIPLICATIVE;\n } else if (blendModeStr === 'BLENDMODE_ONEONE') {\n blendMode = pc.BLEND_ADDITIVE;\n } else if (blendModeStr === 'BLENDMODE_ADD' || blendModeStr === 'BLENDMODE_MULTIPLYADD') {\n blendMode = pc.BLEND_ADDITIVEALPHA;\n }\n\n // Build world velocity graph with gravity\n const gravityX = gravity.x || 0;\n const gravityY = gravity.y || 0; // Don't default to -9.8, use config value\n const gravityZ = -(gravity.z || 0); // Negate Z for coordinate conversion\n\n // Calculate accumulated velocity at end of lifetime due to gravity\n const endVelX = gravityX * lifetime;\n const endVelY = gravityY * lifetime;\n const endVelZ = gravityZ * lifetime;\n\n // Get angular speed range (matching HTML export minAngularSpeed/maxAngularSpeed)\n const minAngularSpeed = psConfig.minAngularSpeed ?? 0;\n const maxAngularSpeed = psConfig.maxAngularSpeed ?? (psConfig.angularSpeed ?? 0);\n\n // Get initial rotation range (matching HTML export minInitialRotation/maxInitialRotation)\n const minInitialRotation = psConfig.minInitialRotation ?? 0;\n const maxInitialRotation = psConfig.maxInitialRotation ?? 0;\n\n // Get scale ranges (matching HTML export minScaleX/maxScaleX/minScaleY/maxScaleY)\n const minSize = psConfig.minSize ?? 0.1;\n const maxSize = psConfig.maxSize ?? 0.3;\n const minScaleX = psConfig.minScaleX ?? 1;\n const maxScaleX = psConfig.maxScaleX ?? 1;\n const minScaleY = psConfig.minScaleY ?? 1;\n const maxScaleY = psConfig.maxScaleY ?? 1;\n\n // Calculate emit power range\n const minEmitPower = psConfig.minEmitPower ?? 1;\n const maxEmitPower = psConfig.maxEmitPower ?? 2;\n\n // Calculate direction-based velocity (average of direction1 and direction2)\n const avgDirX = (direction1.x + direction2.x) / 2;\n const avgDirY = (direction1.y + direction2.y) / 2;\n const avgDirZ = -(direction1.z + direction2.z) / 2; // Negate Z\n\n // Create particle system with enhanced parameters (full feature parity)\n entity.addComponent('particlesystem', {\n numParticles: psConfig.numParticles || 500,\n lifetime: lifetime,\n rate: psConfig.emitRate || 50,\n emitterShape: emitterShape,\n emitterExtents: emitterExtents,\n emitterRadius: psConfig.emitterRadius || 0.1,\n\n // Initial rotation range (matching HTML export)\n startAngle: minInitialRotation,\n startAngle2: maxInitialRotation !== 0 ? maxInitialRotation : 360,\n\n // Speed (radial velocity)\n radialSpeedGraph: new pc.Curve([0, minEmitPower, 1, maxEmitPower]),\n\n // Local space velocity (for directional emission using direction1/direction2)\n localVelocityGraph: new pc.CurveSet([\n [0, avgDirX * minEmitPower],\n [0, avgDirY * minEmitPower],\n [0, avgDirZ * minEmitPower]\n ]),\n localVelocityGraph2: new pc.CurveSet([\n [0, avgDirX * maxEmitPower],\n [0, avgDirY * maxEmitPower],\n [0, avgDirZ * maxEmitPower]\n ]),\n\n // World velocity (gravity effect over lifetime)\n velocityGraph: new pc.CurveSet([\n [0, 0, 1, endVelX],\n [0, 0, 1, endVelY],\n [0, 0, 1, endVelZ]\n ]),\n\n // Scale over lifetime with X/Y scale support\n scaleGraph: new pc.Curve([0, minSize * minScaleX, 1, maxSize * maxScaleX]),\n scaleGraph2: new pc.Curve([0, minSize * minScaleY, 1, maxSize * maxScaleY]),\n\n // Rotation speed range over lifetime (matching HTML export minAngularSpeed/maxAngularSpeed)\n rotationSpeedGraph: new pc.Curve([0, minAngularSpeed]),\n rotationSpeedGraph2: maxAngularSpeed !== minAngularSpeed\n ? new pc.Curve([0, maxAngularSpeed])\n : undefined,\n\n // Color over lifetime with colorDead (RGB) - start -> middle -> dead\n colorGraph: new pc.CurveSet([\n [0, color1.r, 0.5, color2.r, 1, colorDead.r],\n [0, color1.g, 0.5, color2.g, 1, colorDead.g],\n [0, color1.b, 0.5, color2.b, 1, colorDead.b]\n ]),\n\n // Alpha over lifetime with colorDead alpha\n alphaGraph: new pc.Curve([\n 0, color1.a ?? 1,\n 0.5, color2.a ?? 0.8,\n 1, colorDead.a ?? 0\n ]),\n\n // Rendering settings\n blend: blendMode,\n depthWrite: psConfig.depthWrite ?? false,\n depthSoftening: psConfig.softParticles ?? 0,\n lighting: psConfig.lighting ?? false,\n halfLambert: psConfig.halfLambert ?? false,\n alignToMotion: psConfig.alignToMotion ?? false,\n stretch: psConfig.stretch || 0,\n preWarm: psConfig.preWarm ?? false,\n loop: psConfig.loop ?? true,\n autoPlay: psConfig.autoPlay ?? true,\n\n // Sorting and orientation\n sort: psConfig.sort ?? 0,\n orientation: psConfig.orientation ?? 0\n });\n\n // Set local space mode\n if (entity.particlesystem) {\n entity.particlesystem.localSpace = psConfig.localSpace ?? false;\n }\n\n // Set position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n // Add emitter offset for box emitter positioning\n entity.setPosition(\n emitterPos.x + emitterOffset.x,\n emitterPos.y + emitterOffset.y,\n -emitterPos.z + emitterOffset.z\n );\n\n console.log(`[Particle] Entity configured at position: (${emitterPos.x + emitterOffset.x}, ${emitterPos.y + emitterOffset.y}, ${-emitterPos.z + emitterOffset.z})`);\n console.log(`[Particle] Emitter shape: ${psConfig.emitterShape || 'box'}, extents: ${emitterExtents.x}, ${emitterExtents.y}, ${emitterExtents.z}`);\n console.log(`[Particle] Gravity: (${gravityX}, ${gravityY}, ${gravityZ})`);\n console.log(`[Particle] Colors: start=${JSON.stringify(color1)}, mid=${JSON.stringify(color2)}, dead=${JSON.stringify(colorDead)}`);\n console.log(`[Particle] Angular speed: ${minAngularSpeed} - ${maxAngularSpeed}, Initial rotation: ${minInitialRotation} - ${maxInitialRotation}`);\n\n return entity;\n }\n\n /**\n * Initialize all particle systems from config\n */\n async function initParticleSystems(): Promise<void> {\n console.log('═══════════════════════════════════════');\n console.log('🎆 PARTICLE SYSTEM INITIALIZATION');\n console.log('═══════════════════════════════════════');\n console.log('[Particle] config.particles:', config.particles);\n console.log('[Particle] Type:', typeof config.particles);\n console.log('[Particle] Is Array:', Array.isArray(config.particles));\n\n if (!config.particles || config.particles.length === 0) {\n console.log('[Particle] ⚠️ No particle systems to create (particles array is empty or undefined)');\n console.log('═══════════════════════════════════════');\n return;\n }\n\n console.log(`[Particle] 📋 Found ${config.particles.length} particle system(s) to create`);\n\n for (let i = 0; i < config.particles.length; i++) {\n const ps = config.particles[i];\n console.log(`[Particle] --- Particle System ${i + 1}/${config.particles.length} ---`);\n console.log('[Particle] Raw config:', JSON.stringify(ps, null, 2));\n\n try {\n // Get texture URL - support custom textures\n const textureName = ps.particleTexture || 'flare';\n let textureUrl: string;\n let textureCacheKey: string;\n\n if (textureName === 'custom' && ps.customTextureUrl) {\n // Use custom texture URL\n textureUrl = ps.customTextureUrl;\n textureCacheKey = `custom_${ps.id || ps.name || i}`;\n console.log(`[Particle] Custom texture: ${textureUrl.substring(0, 60)}...`);\n } else {\n // Use predefined texture\n textureUrl = PARTICLE_TEXTURE_URLS[textureName] || PARTICLE_TEXTURE_URLS['flare'];\n textureCacheKey = textureName;\n console.log(`[Particle] Texture: ${textureName} -> ${textureUrl.substring(0, 60)}...`);\n }\n\n // Load texture\n console.log('[Particle] Loading texture...');\n const texture = await loadParticleTexture(textureCacheKey, textureUrl);\n console.log('[Particle] ✅ Texture loaded:', texture.name);\n\n // Create particle system entity\n console.log('[Particle] Creating entity...');\n const entity = createParticleSystemEntity(ps);\n console.log('[Particle] ✅ Entity created:', entity.name);\n\n // Set the texture on the particle system\n if (entity.particlesystem) {\n entity.particlesystem.colorMap = texture;\n console.log('[Particle] ✅ Texture applied to particle system');\n console.log('[Particle] Particle system properties:', {\n numParticles: entity.particlesystem.numParticles,\n lifetime: entity.particlesystem.lifetime,\n rate: entity.particlesystem.rate,\n loop: entity.particlesystem.loop,\n autoPlay: entity.particlesystem.autoPlay\n });\n } else {\n console.warn('[Particle] ⚠️ Entity has no particlesystem component!');\n }\n\n // Add to scene\n app.root.addChild(entity);\n console.log('[Particle] ✅ Entity added to scene');\n\n // Log entity position\n const pos = entity.getPosition();\n console.log(`[Particle] Position: (${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`);\n\n // Store reference\n const safeName = (ps.id || ps.name || `particle-${particleEntities.size}`).replace(/[^a-zA-Z0-9]/g, '_');\n particleEntities.set(safeName, entity);\n\n console.log(`[Particle] ✅ SUCCESS: Created \"${ps.name || safeName}\"`);\n } catch (err: any) {\n console.error(`[Particle] ❌ FAILED to create particle system: ${ps.name}`);\n console.error(`[Particle] Error message: ${err?.message || 'No message'}`);\n console.error(`[Particle] Error stack: ${err?.stack || 'No stack'}`);\n console.error(`[Particle] Error object:`, err);\n }\n }\n\n console.log('═══════════════════════════════════════');\n console.log(`[Particle] 🎉 COMPLETE: ${particleEntities.size}/${config.particles.length} particle systems created`);\n console.log('[Particle] Active particle systems:', Array.from(particleEntities.keys()));\n console.log('═══════════════════════════════════════');\n }\n\n /**\n * Stop all particle systems\n */\n function stopAllParticles(): void {\n particleEntities.forEach((entity) => {\n if (entity.particlesystem) {\n entity.particlesystem.stop();\n }\n });\n }\n\n /**\n * Start all particle systems\n */\n function startAllParticles(): void {\n particleEntities.forEach((entity) => {\n if (entity.particlesystem) {\n entity.particlesystem.play();\n }\n });\n }\n\n /**\n * Toggle a specific particle system by ID\n */\n function toggleParticle(id: string): void {\n const entity = particleEntities.get(id);\n if (entity) {\n entity.enabled = !entity.enabled;\n }\n }\n\n // =====================================================\n // CUSTOM MESH SYSTEM (3D Models - GLB/GLTF)\n // =====================================================\n interface CustomMeshData {\n entity: pc.Entity;\n config: any;\n animComponent?: any;\n audioSlotId?: string;\n isAnimPlaying: boolean;\n audioPlaying: boolean;\n }\n\n const customMeshEntities: Map<string, CustomMeshData> = new Map();\n\n /**\n * Helper to play an animation component using various methods\n * PlayCanvas has different ways to start animations depending on the setup\n */\n function playAnimComponent(anim: any): void {\n try {\n // Method 1: Set playing property (simple animations)\n anim.playing = true;\n\n // Method 2: Use baseLayer.play() if available (Anim component)\n if (anim.baseLayer && typeof anim.baseLayer.play === 'function') {\n // Get the first available state/clip name\n const states = anim.baseLayer.states;\n if (states && states.length > 0) {\n const firstState = states[0];\n if (firstState && firstState !== 'START' && firstState !== 'END' && firstState !== 'ANY') {\n anim.baseLayer.play(firstState);\n console.log('[CustomMesh] Playing animation state:', firstState);\n }\n }\n }\n\n // Method 3: Direct play() method\n if (typeof anim.play === 'function') {\n anim.play();\n }\n\n // Method 4: Set speed to ensure animation runs\n if (anim.speed !== undefined) {\n anim.speed = 1;\n }\n\n console.log('[CustomMesh] Animation started, playing:', anim.playing);\n } catch (err) {\n console.error('[CustomMesh] Error playing animation:', err);\n }\n }\n\n /**\n * Helper to pause an animation component\n */\n function pauseAnimComponent(anim: any): void {\n try {\n anim.playing = false;\n if (anim.speed !== undefined) {\n anim.speed = 0;\n }\n if (typeof anim.pause === 'function') {\n anim.pause();\n }\n } catch (err) {\n console.error('[CustomMesh] Error pausing animation:', err);\n }\n }\n\n /**\n * Sample an animation track at a specific time\n * Handles keyframe interpolation for position, rotation, and scale\n */\n function sampleAnimationTrack(track: any, time: number): any {\n try {\n const keys = track.keys || track.keyframes || [];\n if (keys.length === 0) return null;\n\n // Find surrounding keyframes\n let prevKey = keys[0];\n let nextKey = keys[keys.length - 1];\n\n for (let i = 0; i < keys.length - 1; i++) {\n const currentTime = keys[i].time ?? keys[i][0];\n const nextTime = keys[i + 1].time ?? keys[i + 1][0];\n if (time >= currentTime && time < nextTime) {\n prevKey = keys[i];\n nextKey = keys[i + 1];\n break;\n }\n }\n\n // Calculate interpolation factor\n const prevTime = prevKey.time ?? prevKey[0] ?? 0;\n const nextTime = nextKey.time ?? nextKey[0] ?? 0;\n const duration = nextTime - prevTime;\n const t = duration > 0 ? (time - prevTime) / duration : 0;\n\n // Get values (handle both object and array formats)\n const prevValue = prevKey.value ?? prevKey.slice?.(1) ?? prevKey;\n const nextValue = nextKey.value ?? nextKey.slice?.(1) ?? nextKey;\n\n // Interpolate based on track type\n const trackType = track.type || track.property || 'position';\n\n if (trackType.includes('rotation') || trackType.includes('quaternion')) {\n // Quaternion SLERP interpolation\n if (Array.isArray(prevValue) && prevValue.length >= 4) {\n return slerpQuat(prevValue, nextValue, t);\n }\n return prevValue;\n } else {\n // Linear interpolation for position/scale\n if (Array.isArray(prevValue)) {\n return prevValue.map((v: number, i: number) => {\n const nv = nextValue[i] ?? v;\n return v + (nv - v) * t;\n });\n }\n return prevValue;\n }\n } catch (e) {\n return null;\n }\n }\n\n /**\n * Simple quaternion SLERP\n */\n function slerpQuat(a: number[], b: number[], t: number): number[] {\n // Calculate cosine of angle\n let cosom = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];\n\n // Adjust signs if necessary\n const bAdj = [...b];\n if (cosom < 0) {\n cosom = -cosom;\n bAdj[0] = -bAdj[0];\n bAdj[1] = -bAdj[1];\n bAdj[2] = -bAdj[2];\n bAdj[3] = -bAdj[3];\n }\n\n // Calculate coefficients\n let scale0: number, scale1: number;\n if (1 - cosom > 0.0001) {\n const omega = Math.acos(cosom);\n const sinom = Math.sin(omega);\n scale0 = Math.sin((1 - t) * omega) / sinom;\n scale1 = Math.sin(t * omega) / sinom;\n } else {\n // Use linear interpolation for very close quaternions\n scale0 = 1 - t;\n scale1 = t;\n }\n\n return [\n scale0 * a[0] + scale1 * bAdj[0],\n scale0 * a[1] + scale1 * bAdj[1],\n scale0 * a[2] + scale1 * bAdj[2],\n scale0 * a[3] + scale1 * bAdj[3]\n ];\n }\n\n /**\n * Apply an animation value to a target entity\n */\n function applyAnimationValue(rootEntity: pc.Entity, targetPath: string, trackType: string, value: any): void {\n try {\n // Parse the target path (e.g., \"Bone001/position\" or just \"position\")\n const parts = targetPath.split('/');\n const property = parts.pop() || targetPath;\n\n // Find the target node\n let target: pc.Entity | null = rootEntity;\n if (parts.length > 0) {\n const nodeName = parts.join('/');\n target = rootEntity.findByName(nodeName) as pc.Entity || rootEntity;\n }\n\n if (!target) return;\n\n // Apply the value based on property type\n const normalizedProp = property.toLowerCase();\n\n if (normalizedProp.includes('position') || normalizedProp.includes('translation')) {\n if (Array.isArray(value) && value.length >= 3) {\n target.setLocalPosition(value[0], value[1], value[2]);\n }\n } else if (normalizedProp.includes('rotation') || normalizedProp.includes('quaternion')) {\n if (Array.isArray(value) && value.length >= 4) {\n target.setLocalRotation(new pc.Quat(value[0], value[1], value[2], value[3]));\n } else if (Array.isArray(value) && value.length >= 3) {\n // Euler angles\n target.setLocalEulerAngles(value[0], value[1], value[2]);\n }\n } else if (normalizedProp.includes('scale')) {\n if (Array.isArray(value) && value.length >= 3) {\n target.setLocalScale(value[0], value[1], value[2]);\n }\n }\n } catch (e) {\n // Silently ignore - animation track may reference non-existent node\n }\n }\n\n /**\n * Enhanced animation player that handles both anim and animation components\n * Also handles the new format with type info including GLB animations\n */\n function playAnimComponentV2(animInfo: any): void {\n const { type, component, node, animations } = animInfo;\n console.log('[CustomMesh] Playing animation, type:', type, 'node:', node?.name, 'animations:', animations?.length);\n\n try {\n if (type === 'pc-anim') {\n // Simple PlayCanvas anim component - matches HTML export approach\n console.log('[CustomMesh] Using simple pc-anim approach (like HTML export)');\n component.playing = true;\n animInfo.isPlaying = true;\n console.log('[CustomMesh] Animation started - playing:', component.playing);\n } else if (type === 'animation') {\n // Legacy Animation component\n component.playing = true;\n component.loop = true;\n if (typeof component.play === 'function') {\n // Play the first animation clip\n const clips = Object.keys(component.animations || {});\n if (clips.length > 0) {\n component.play(clips[0], 1);\n console.log('[CustomMesh] Playing legacy animation clip:', clips[0]);\n } else {\n component.play();\n }\n }\n } else if (type === 'glb-skeleton') {\n // GLB skeleton animation - use PlayCanvas AnimComponent\n console.log('[CustomMesh] Starting GLB skeleton animation playback');\n const { modelEntity, asset, animations: animList } = animInfo;\n\n if (animList && animList.length > 0) {\n const firstAnim = animList[0];\n console.log('[CustomMesh] Animation clip:', firstAnim);\n console.log('[CustomMesh] Animation name:', firstAnim._name || firstAnim.name);\n console.log('[CustomMesh] Animation duration:', firstAnim._duration || firstAnim.duration);\n\n try {\n // Try to add AnimComponent if not exists\n if (!modelEntity.anim) {\n console.log('[CustomMesh] Adding AnimComponent to modelEntity');\n\n // Create simple state graph for the animation\n const stateGraphData = {\n layers: [{\n name: 'Base',\n states: [\n { name: 'START', speed: 1 },\n { name: 'Idle', speed: 1, loop: true }\n ],\n transitions: [{\n from: 'START',\n to: 'Idle',\n time: 0,\n conditions: []\n }]\n }]\n };\n\n modelEntity.addComponent('anim', {\n activate: true,\n speed: 1\n });\n\n // Load the state graph\n if (modelEntity.anim) {\n modelEntity.anim.loadStateGraph(stateGraphData);\n\n // Assign the animation to the Idle state\n const animAsset = firstAnim;\n modelEntity.anim.assignAnimation('Base.Idle', animAsset, 'Base');\n\n console.log('[CustomMesh] AnimComponent added and animation assigned');\n }\n }\n\n // Now play via the anim component\n if (modelEntity.anim) {\n modelEntity.anim.playing = true;\n modelEntity.anim.speed = 1;\n animInfo.animComponent = modelEntity.anim;\n animInfo.isPlaying = true;\n console.log('[CustomMesh] GLB animation playing via AnimComponent');\n } else {\n // Fallback: Manual curve sampling using PlayCanvas AnimTrack format\n console.log('[CustomMesh] Falling back to manual curve sampling');\n animInfo.isPlaying = true;\n animInfo.startTime = Date.now();\n\n const updateHandler = (dt: number) => {\n if (!animInfo.isPlaying) return;\n\n const elapsed = (Date.now() - animInfo.startTime) / 1000;\n const duration = firstAnim._duration || firstAnim.duration || 1;\n const loopedTime = elapsed % duration;\n\n // Sample using PlayCanvas AnimTrack._curves\n try {\n const curves = firstAnim._curves || [];\n curves.forEach((curve: any, idx: number) => {\n if (!curve) return;\n\n // Get the target path from AnimTrack\n const paths = firstAnim._paths?.[idx];\n if (!paths || !paths.entityPath) return;\n\n // Find target entity\n let target = modelEntity.findByPath(paths.entityPath.join('/'));\n if (!target) target = modelEntity.findByName(paths.entityPath[paths.entityPath.length - 1]);\n if (!target) return;\n\n // Sample the curve at current time\n const value = curve.evaluate?.(loopedTime);\n if (value === undefined || value === null) return;\n\n // Apply based on property type\n const prop = paths.component || paths.propertyPath?.[0];\n if (prop === 'localPosition' || prop === 'position') {\n if (Array.isArray(value)) {\n target.setLocalPosition(value[0], value[1], value[2]);\n }\n } else if (prop === 'localRotation' || prop === 'rotation') {\n if (Array.isArray(value) && value.length >= 4) {\n target.setLocalRotation(new pc.Quat(value[0], value[1], value[2], value[3]));\n }\n } else if (prop === 'localScale' || prop === 'scale') {\n if (Array.isArray(value)) {\n target.setLocalScale(value[0], value[1], value[2]);\n }\n }\n });\n } catch (e) {\n // Silently ignore - curve evaluation errors\n }\n };\n\n animInfo.updateHandler = updateHandler;\n app.on('update', updateHandler);\n console.log('[CustomMesh] Manual animation playback started');\n }\n } catch (err) {\n console.error('[CustomMesh] Error setting up GLB animation:', err);\n }\n }\n } else if (type === 'anim') {\n // New Anim component\n component.playing = true;\n component.speed = 1;\n\n // Try to play via baseLayer\n if (component.baseLayer) {\n const states = component.baseLayer.states || [];\n const playableState = states.find((s: string) => s !== 'START' && s !== 'END' && s !== 'ANY');\n if (playableState) {\n component.baseLayer.play(playableState);\n console.log('[CustomMesh] Playing anim state:', playableState);\n }\n }\n }\n\n // Only log component state if component exists (not for glb-skeleton)\n if (component) {\n console.log('[CustomMesh] Animation component state - playing:', component.playing, 'speed:', component.speed);\n }\n } catch (err) {\n console.error('[CustomMesh] Error in playAnimComponentV2:', err);\n }\n }\n\n /**\n * Enhanced animation pauser\n */\n function pauseAnimComponentV2(animInfo: any): void {\n const { type, component } = animInfo;\n\n try {\n if (type === 'pc-anim') {\n // Simple PlayCanvas anim component - matches HTML export approach\n component.playing = false;\n animInfo.isPlaying = false;\n console.log('[CustomMesh] pc-anim animation paused');\n } else if (type === 'glb-skeleton') {\n // Stop GLB skeleton animation\n animInfo.isPlaying = false;\n\n // If we have an AnimComponent, pause it\n if (animInfo.animComponent) {\n animInfo.animComponent.playing = false;\n console.log('[CustomMesh] GLB skeleton animation paused via AnimComponent');\n }\n\n // Remove update handler if using manual playback\n if (animInfo.updateHandler) {\n app.off('update', animInfo.updateHandler);\n animInfo.updateHandler = null;\n console.log('[CustomMesh] GLB manual animation paused');\n }\n } else if (component) {\n component.playing = false;\n component.speed = 0;\n\n if (type === 'animation' && typeof component.pause === 'function') {\n component.pause();\n }\n }\n } catch (err) {\n console.error('[CustomMesh] Error pausing animation:', err);\n }\n }\n\n /**\n * Load and create a custom mesh entity (GLB/GLTF model)\n * Matches HTML export customMeshSystem.ts\n */\n function loadCustomMesh(meshConfig: any, index: number): pc.Entity | null {\n console.log('[CustomMesh] Loading mesh', index, ':', meshConfig.name, meshConfig);\n\n // Validate modelUrl before attempting to load\n if (!meshConfig.modelUrl || typeof meshConfig.modelUrl !== 'string' || meshConfig.modelUrl.trim() === '') {\n console.warn('[CustomMesh] Skipping mesh', meshConfig.name, '- no valid modelUrl provided. Config:', meshConfig);\n return null;\n }\n\n // Create container entity\n const entity = new pc.Entity('custom-mesh-' + index);\n\n // Position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n const pos = meshConfig.position || { x: 0, y: 0, z: 0 };\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n\n // Rotation (convert from radians to degrees)\n if (meshConfig.rotation) {\n const rot = meshConfig.rotation;\n const radToDeg = 180 / Math.PI;\n entity.setEulerAngles(\n (rot._x ?? rot.x ?? 0) * radToDeg,\n (rot._y ?? rot.y ?? 0) * radToDeg,\n (rot._z ?? rot.z ?? 0) * radToDeg\n );\n }\n\n // Scale\n if (meshConfig.scale) {\n const scale = meshConfig.scale;\n entity.setLocalScale(\n scale._x ?? scale.x ?? 1,\n scale._y ?? scale.y ?? 1,\n scale._z ?? scale.z ?? 1\n );\n }\n\n // Load GLB/GLTF model\n const modelUrl = meshConfig.modelUrl.trim();\n console.log('[CustomMesh] Loading model from URL:', modelUrl);\n\n const modelAsset = new pc.Asset(\n 'mesh-model-' + index,\n 'container',\n { url: modelUrl }\n );\n\n app.assets.add(modelAsset);\n\n // Store mesh data reference\n const meshId = meshConfig.id || `mesh-${index}`;\n const meshData: CustomMeshData = {\n entity,\n config: meshConfig,\n isAnimPlaying: false,\n audioPlaying: false\n };\n customMeshEntities.set(meshId, meshData);\n\n modelAsset.ready((asset: any) => {\n try {\n console.log('[CustomMesh] Model loaded:', meshConfig.name);\n\n // Validate asset resource before instantiation\n if (!asset || !asset.resource) {\n console.error('[CustomMesh] Invalid asset resource for mesh:', meshConfig.name);\n return;\n }\n\n // Instantiate the model\n const modelEntity = asset.resource.instantiateRenderEntity();\n if (!modelEntity) {\n console.error('[CustomMesh] Failed to instantiate render entity for mesh:', meshConfig.name);\n return;\n }\n\n entity.addChild(modelEntity);\n\n // Store reference to model entity for animation\n (entity as any).modelEntity = modelEntity;\n\n // IMPORTANT: Recursively enable all children in the hierarchy\n // Some GLB/GLTF files have nodes disabled by default\n function enableAllChildren(node: any): void {\n node.enabled = true;\n if (node.children) {\n node.children.forEach((child: any) => enableAllChildren(child));\n }\n }\n enableAllChildren(modelEntity);\n\n // Count children for debugging\n let childCount = 0;\n modelEntity.forEach(() => { childCount++; });\n console.log('[CustomMesh] Enabled all children for:', meshConfig.name, '- Total nodes:', childCount);\n\n // Apply static opacity if specified (not animated mode)\n if (meshConfig.opacityMode !== 'animated' && meshConfig.opacity !== undefined && meshConfig.opacity < 1) {\n // Apply opacity to all mesh instances\n applyOpacityToCustomMesh(entity, meshConfig.opacity);\n console.log('[CustomMesh] Applied static opacity:', meshConfig.opacity, 'to', meshConfig.name);\n }\n\n // Setup billboarding if enabled (with range support)\n if (meshConfig.billboard) {\n // Store original rotation for when billboard is disabled\n const originalRotation = entity.getEulerAngles().clone();\n\n // Initialize billboard active state\n (entity as any)._billboardActive = !meshConfig.billboardRange; // Active by default if no range\n\n // Create update handler to make mesh face camera when billboard is active\n app.on('update', () => {\n if (!entity.enabled) return;\n\n // Check if billboard should be active (controlled by updateCustomMeshVisibility)\n const billboardActive = (entity as any)._billboardActive;\n if (!billboardActive) {\n // Restore original rotation when billboard is disabled\n entity.setEulerAngles(originalRotation.x, originalRotation.y, originalRotation.z);\n return;\n }\n\n const camPos = camera.getPosition();\n const meshPos = entity.getPosition();\n\n // Calculate angle to camera (Y-axis rotation only for billboard)\n const dx = camPos.x - meshPos.x;\n const dz = camPos.z - meshPos.z;\n const angle = Math.atan2(dx, dz) * (180 / Math.PI);\n\n // Get current rotation and only update Y\n const currentRot = entity.getEulerAngles();\n entity.setEulerAngles(currentRot.x, angle, currentRot.z);\n });\n console.log('[CustomMesh] Billboard enabled for:', meshConfig.name, meshConfig.billboardRange ? '(with range control)' : '(always active)');\n }\n\n // Handle animations - ALWAYS check for GLB animations regardless of interaction settings\n // Log what's available in the asset for debugging\n const hasGlbAnimations = asset.resource?.animations?.length > 0;\n console.log('[CustomMesh] Asset resource for', meshConfig.name, ':', {\n hasAnimations: hasGlbAnimations,\n animationCount: asset.resource?.animations?.length || 0,\n animations: asset.resource?.animations?.map((a: any) => a?.name || a?.resource?.name || 'unnamed') || [],\n interactionConfig: meshConfig.interaction\n });\n\n // Setup animations if the model has them\n // APPROACH: Match HTML export - prioritize modelEntity.anim first, then fallback\n if (hasGlbAnimations || (meshConfig.interaction && meshConfig.interaction.playModelAnimation)) {\n const allAnimComponents: any[] = [];\n\n // PRIORITY 1: Check if PlayCanvas auto-created an anim component (like HTML export expects)\n const animComponent = (modelEntity as any).anim;\n if (animComponent) {\n console.log('[CustomMesh] Found modelEntity.anim component - using simple approach');\n allAnimComponents.push({\n type: 'pc-anim',\n component: animComponent,\n modelEntity: modelEntity\n });\n }\n\n // PRIORITY 2: Search hierarchy for anim/animation components\n if (allAnimComponents.length === 0) {\n function findAllAnimComponents(node: any, depth: number = 0): void {\n // Check for new Anim component\n if (node.anim && !allAnimComponents.find((a: any) => a.component === node.anim)) {\n allAnimComponents.push({ type: 'anim', component: node.anim, node: node });\n console.log('[CustomMesh] Found existing ANIM component on:', node.name);\n }\n\n // Check for legacy Animation component\n if (node.animation) {\n allAnimComponents.push({ type: 'animation', component: node.animation, node: node });\n console.log('[CustomMesh] Found ANIMATION (legacy) component on:', node.name);\n }\n\n if (node.children) {\n node.children.forEach((child: any) => findAllAnimComponents(child, depth + 1));\n }\n }\n findAllAnimComponents(modelEntity);\n }\n\n // PRIORITY 3: Fall back to manual GLB skeleton animation approach\n if (allAnimComponents.length === 0 && hasGlbAnimations) {\n const animations = asset.resource.animations;\n console.log('[CustomMesh] No anim component found - falling back to GLB skeleton approach');\n console.log('[CustomMesh] GLB has', animations.length, 'embedded animations');\n\n try {\n allAnimComponents.push({\n type: 'glb-skeleton',\n modelEntity: modelEntity,\n asset: asset,\n animations: animations,\n animationNames: animations.map((a: any) => a.resource?.name || a.name || 'Animation'),\n isPlaying: false,\n currentTime: 0\n });\n\n console.log('[CustomMesh] GLB skeleton animations stored:',\n animations.map((a: any) => a.resource?.name || a.name));\n\n } catch (err) {\n console.error('[CustomMesh] Error setting up GLB animations:', err);\n }\n }\n\n if (allAnimComponents.length > 0) {\n // Store all animation components (now with type info)\n (meshData as any).allAnimComponents = allAnimComponents;\n meshData.animComponent = allAnimComponents[0].component;\n\n console.log('[CustomMesh] Total animation components for', meshConfig.name, ':', allAnimComponents.length,\n '- Types:', allAnimComponents.map((a: any) => a.type).join(', '));\n\n // Auto-play if enabled OR if animationAutoPlay is set\n const shouldAutoPlay = meshConfig.interaction?.animationAutoPlay;\n if (shouldAutoPlay) {\n allAnimComponents.forEach((animInfo: any) => {\n playAnimComponentV2(animInfo);\n });\n meshData.isAnimPlaying = true;\n console.log('[CustomMesh] Auto-playing all animations for:', meshConfig.name);\n }\n } else {\n console.warn('[CustomMesh] No animation components could be set up for mesh:', meshConfig.name);\n }\n } else {\n console.log('[CustomMesh] No animations to setup for:', meshConfig.name, '(no GLB animations and playModelAnimation not enabled)');\n }\n\n // Setup spatial audio if configured\n if (meshConfig.interaction && meshConfig.interaction.playAudio && meshConfig.interaction.audioUrl) {\n setupMeshAudio(entity, meshConfig, meshData);\n }\n\n // Setup click interaction\n if (meshConfig.interaction) {\n setupMeshClickInteraction(entity, meshConfig, meshData);\n }\n\n console.log('[CustomMesh] Mesh fully initialized:', meshConfig.name);\n } catch (err) {\n console.error('[CustomMesh] Error processing loaded mesh:', meshConfig.name, err);\n }\n });\n\n modelAsset.on('error', (err: any) => {\n console.error('[CustomMesh] Failed to load mesh:', meshConfig.name, err);\n // Remove failed asset to prevent further issues\n app.assets.remove(modelAsset);\n });\n\n app.assets.load(modelAsset);\n\n // Visibility range setup\n if (meshConfig.visibilityRange) {\n (entity as any).visibilityRange = meshConfig.visibilityRange;\n entity.enabled = meshConfig.enabled !== false;\n } else {\n entity.enabled = meshConfig.enabled !== false;\n }\n\n // Opacity configuration\n (entity as any).opacityConfig = {\n mode: meshConfig.opacityMode || 'static',\n value: meshConfig.opacity !== undefined ? meshConfig.opacity : 1\n };\n\n app.root.addChild(entity);\n\n return entity;\n }\n\n /**\n * Setup spatial audio for mesh\n */\n function setupMeshAudio(entity: pc.Entity, meshConfig: any, meshData: CustomMeshData): void {\n const audioConfig = meshConfig.interaction;\n const meshId = meshConfig.id || meshConfig.name;\n const slotId = `mesh-audio-${meshId}`;\n\n // Add sound component to entity\n entity.addComponent('sound', {\n positional: audioConfig.audioSpatial || false,\n distanceModel: audioConfig.audioDistanceModel || 'exponential', // Match HTML export default\n refDistance: audioConfig.audioRefDistance || 1,\n maxDistance: audioConfig.audioMaxDistance || 100,\n rollOffFactor: audioConfig.audioRollOffFactor || 1,\n slots: {\n [slotId]: {\n name: slotId,\n loop: audioConfig.audioLoop || false,\n autoPlay: false,\n volume: audioConfig.audioVolume !== undefined ? audioConfig.audioVolume : 1,\n pitch: 1\n }\n }\n });\n\n meshData.audioSlotId = slotId;\n\n // Load audio asset\n const audioAsset = new pc.Asset(\n `mesh-audio-asset-${meshId}`,\n 'audio',\n { url: audioConfig.audioUrl }\n );\n\n app.assets.add(audioAsset);\n\n audioAsset.ready(() => {\n const slot = entity.sound?.slot(slotId);\n if (slot) {\n slot.asset = audioAsset.id;\n }\n console.log('[CustomMesh] Audio loaded for mesh:', meshConfig.name, 'Spatial:', audioConfig.audioSpatial);\n });\n\n audioAsset.on('error', (err: any) => {\n console.error('[CustomMesh] Failed to load mesh audio:', meshConfig.name, err);\n });\n\n app.assets.load(audioAsset);\n }\n\n /**\n * Check if a ray intersects any mesh render component in the entity hierarchy\n */\n function rayIntersectsMeshHierarchy(entity: pc.Entity, rayOrigin: pc.Vec3, rayDir: pc.Vec3): boolean {\n // Check this entity's render components\n const render = (entity as any).render;\n if (render && render.meshInstances) {\n for (const mi of render.meshInstances) {\n if (mi.aabb) {\n const intersection = rayIntersectsAABB(rayOrigin, rayDir, mi.aabb);\n if (intersection) return true;\n }\n }\n }\n\n // Check model component if present\n const model = (entity as any).model;\n if (model && model.meshInstances) {\n for (const mi of model.meshInstances) {\n if (mi.aabb) {\n const intersection = rayIntersectsAABB(rayOrigin, rayDir, mi.aabb);\n if (intersection) return true;\n }\n }\n }\n\n // Recursively check children\n const children = entity.children;\n if (children) {\n for (const child of children) {\n if (child instanceof pc.Entity && rayIntersectsMeshHierarchy(child, rayOrigin, rayDir)) {\n return true;\n }\n }\n }\n\n return false;\n }\n\n /**\n * Ray-AABB intersection test\n */\n function rayIntersectsAABB(rayOrigin: pc.Vec3, rayDir: pc.Vec3, aabb: any): boolean {\n const min = aabb.getMin ? aabb.getMin() : aabb.min;\n const max = aabb.getMax ? aabb.getMax() : aabb.max;\n\n if (!min || !max) return false;\n\n let tmin = -Infinity;\n let tmax = Infinity;\n\n for (let i = 0; i < 3; i++) {\n const axis = ['x', 'y', 'z'][i];\n const origin = (rayOrigin as any)[axis];\n const dir = (rayDir as any)[axis];\n const minVal = (min as any)[axis] ?? min.data?.[i];\n const maxVal = (max as any)[axis] ?? max.data?.[i];\n\n if (Math.abs(dir) < 1e-8) {\n if (origin < minVal || origin > maxVal) return false;\n } else {\n let t1 = (minVal - origin) / dir;\n let t2 = (maxVal - origin) / dir;\n if (t1 > t2) [t1, t2] = [t2, t1];\n tmin = Math.max(tmin, t1);\n tmax = Math.min(tmax, t2);\n if (tmin > tmax) return false;\n }\n }\n\n return tmax >= 0;\n }\n\n /**\n * Check if a custom mesh has any active interactions\n * Matches HTML export's hasInteractions function\n */\n function hasInteractions(customMesh: any): boolean {\n const interaction = customMesh.interaction;\n if (!interaction) return false;\n return !!(\n interaction.triggerUIPopup ||\n interaction.playAudio ||\n interaction.playModelAnimation ||\n interaction.triggerDirectLink\n );\n }\n\n /**\n * Display mesh content using the hotspot popup system\n * Matches HTML export's displayMeshContent function\n */\n function displayMeshContent(customMesh: any): void {\n if (!customMesh.interaction) return;\n\n // Create a hotspot-like object to use with the hotspot popup system\n const hotspotData = {\n id: `mesh-content-${customMesh.id}`,\n title: customMesh.interaction.title || customMesh.name,\n information: customMesh.interaction.information,\n photoUrl: customMesh.interaction.photoUrl,\n iframeUrl: customMesh.interaction.iframeUrl,\n externalLinkUrl: customMesh.interaction.externalLinkUrl,\n externalLinkText: customMesh.interaction.externalLinkText,\n // Style options\n backgroundColor: customMesh.interaction.backgroundColor || '#000000',\n textColor: customMesh.interaction.textColor || '#ffffff',\n };\n\n // Use the viewer UI's hotspot popup if available, otherwise use our own popup\n if ((uiElements as any).showHotspotPopup) {\n (uiElements as any).showHotspotPopup(hotspotData);\n } else {\n showMeshPopup(customMesh);\n }\n }\n\n /**\n * Hide mesh content popup\n */\n function hideMeshContent(): void {\n // Remove hotspot popup if visible\n const hotspotPopup = document.querySelector('.storysplat-hotspot-popup');\n if (hotspotPopup) hotspotPopup.remove();\n\n // Remove mesh popup if visible\n const meshPopup = document.querySelector('.storysplat-mesh-popup');\n if (meshPopup) meshPopup.remove();\n }\n\n /**\n * Handle direct link trigger for a mesh\n * Matches HTML export's handleDirectLink function\n */\n function handleDirectLink(customMesh: any): void {\n if (!customMesh.interaction?.triggerDirectLink || !customMesh.interaction.directLinkUrl) return;\n window.open(customMesh.interaction.directLinkUrl, '_blank');\n }\n\n /**\n * Setup hover and click interactions for mesh\n * Enhanced to match HTML export's setupMeshInteractions with per-interaction trigger modes\n */\n function setupMeshClickInteraction(entity: pc.Entity, meshConfig: any, meshData: CustomMeshData): void {\n // Skip if no active interactions\n if (!hasInteractions(meshConfig)) {\n console.log('[CustomMesh] Skipping interaction setup - no active interactions for:', meshConfig.name);\n return;\n }\n\n // Get the global activation mode (fallback for per-interaction modes)\n const activationMode = meshConfig.interaction?.activationMode || 'click';\n\n // Click handler using canvas click + raycast\n const canvasForMesh = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n // Helper to perform raycast\n const performRaycast = (clientX: number, clientY: number): boolean => {\n const rect = canvasForMesh.getBoundingClientRect();\n const x = clientX - rect.left;\n const y = clientY - rect.top;\n\n // Raycast from camera through click point\n const from = camera.camera!.screenToWorld(x, y, camera.camera!.nearClip);\n const to = camera.camera!.screenToWorld(x, y, camera.camera!.farClip);\n const dir = new pc.Vec3().sub2(to, from).normalize();\n\n // Get the model entity (child of container entity)\n const modelEntity = (entity as any).modelEntity as pc.Entity | undefined;\n\n // Check intersection with model entity hierarchy (includes all children)\n if (modelEntity && rayIntersectsMeshHierarchy(modelEntity, from, dir)) {\n return true;\n }\n\n // Fallback: also check the container entity itself\n if (rayIntersectsMeshHierarchy(entity, from, dir)) {\n return true;\n }\n\n // Final fallback: distance-based check for small or simple meshes\n const meshPos = entity.getPosition();\n const toMesh = new pc.Vec3().sub2(meshPos, from);\n const t = toMesh.dot(dir);\n if (t > 0) {\n const closestPoint = new pc.Vec3().add2(from, dir.clone().mulScalar(t));\n const distance = closestPoint.distance(meshPos);\n const scale = entity.getLocalScale();\n const hitRadius = Math.max(scale.x, scale.y, scale.z) * 1.5; // Larger radius for fallback\n if (distance < hitRadius) {\n return true;\n }\n }\n\n return false;\n };\n\n // Get per-interaction trigger modes (matches HTML export pattern)\n const popupTriggerMode = meshConfig.interaction?.popupTriggerMode || activationMode;\n const audioTriggerMode = meshConfig.interaction?.audioTriggerMode || activationMode;\n const animationTriggerMode = meshConfig.interaction?.animationTriggerMode || activationMode;\n const directLinkTriggerMode = meshConfig.interaction?.directLinkTriggerMode || activationMode;\n\n // Track hover state for this mesh\n let isHovering = false;\n\n /**\n * Handle HOVER IN interactions\n * Matches HTML export's OnPointerOverTrigger behavior\n */\n const handleHoverIn = () => {\n // Change cursor to pointer\n canvasForMesh.style.cursor = 'pointer';\n\n // UI Popup - trigger on hover if popupTriggerMode is 'hover'\n if (popupTriggerMode === 'hover' && meshConfig.interaction?.triggerUIPopup) {\n displayMeshContent(meshConfig);\n }\n\n // Audio - trigger on hover if audioTriggerMode is 'hover'\n if (audioTriggerMode === 'hover' && meshConfig.interaction?.playAudio && meshData.audioSlotId) {\n const slot = entity.sound?.slot(meshData.audioSlotId);\n if (slot && !slot.isPlaying) {\n slot.play();\n meshData.audioPlaying = true;\n console.log('[CustomMesh] Playing audio on hover for:', meshConfig.name);\n }\n }\n\n // Animation - trigger on hover if animationTriggerMode is 'hover'\n if (animationTriggerMode === 'hover' && meshConfig.interaction?.playModelAnimation) {\n const allAnimComponents = (meshData as any).allAnimComponents as any[] | undefined;\n if (allAnimComponents && allAnimComponents.length > 0 && !meshData.isAnimPlaying) {\n allAnimComponents.forEach((animInfo: any) => playAnimComponentV2(animInfo));\n meshData.isAnimPlaying = true;\n console.log('[CustomMesh] Playing animation on hover for:', meshConfig.name);\n }\n }\n\n // Direct link - trigger on hover if directLinkTriggerMode is 'hover'\n if (directLinkTriggerMode === 'hover' && meshConfig.interaction?.triggerDirectLink) {\n handleDirectLink(meshConfig);\n }\n };\n\n /**\n * Handle HOVER OUT interactions\n * Matches HTML export's OnPointerOutTrigger behavior\n */\n const handleHoverOut = () => {\n // Reset cursor\n canvasForMesh.style.cursor = '';\n\n // UI Popup - hide on hover out if popupTriggerMode is 'hover'\n if (popupTriggerMode === 'hover' && meshConfig.interaction?.triggerUIPopup) {\n hideMeshContent();\n }\n\n // Audio - stop on hover out if audioTriggerMode is 'hover'\n if (audioTriggerMode === 'hover' && meshConfig.interaction?.playAudio && meshData.audioSlotId) {\n const slot = entity.sound?.slot(meshData.audioSlotId);\n if (slot && slot.isPlaying) {\n slot.stop();\n meshData.audioPlaying = false;\n console.log('[CustomMesh] Stopped audio on hover out for:', meshConfig.name);\n }\n }\n\n // Animation - pause on hover out if animationTriggerMode is 'hover'\n if (animationTriggerMode === 'hover' && meshConfig.interaction?.playModelAnimation) {\n const allAnimComponents = (meshData as any).allAnimComponents as any[] | undefined;\n if (allAnimComponents && allAnimComponents.length > 0 && meshData.isAnimPlaying) {\n allAnimComponents.forEach((animInfo: any) => pauseAnimComponentV2(animInfo));\n meshData.isAnimPlaying = false;\n console.log('[CustomMesh] Paused animation on hover out for:', meshConfig.name);\n }\n }\n };\n\n /**\n * Handle CLICK interactions\n * Matches HTML export's OnPickTrigger behavior with toggle support\n */\n const handleClick = () => {\n console.log('[CustomMesh] Clicked mesh:', meshConfig.name);\n\n // UI Popup - trigger on click if popupTriggerMode is 'click'\n if (popupTriggerMode === 'click' && meshConfig.interaction?.triggerUIPopup) {\n displayMeshContent(meshConfig);\n }\n\n // Animation - toggle on click if animationTriggerMode is 'click'\n if (animationTriggerMode === 'click' && meshConfig.interaction?.playModelAnimation) {\n const allAnimComponents = (meshData as any).allAnimComponents as any[] | undefined;\n if (allAnimComponents && allAnimComponents.length > 0) {\n if (meshData.isAnimPlaying) {\n allAnimComponents.forEach((animInfo: any) => pauseAnimComponentV2(animInfo));\n meshData.isAnimPlaying = false;\n console.log('[CustomMesh] Paused', allAnimComponents.length, 'animations on click for:', meshConfig.name);\n } else {\n allAnimComponents.forEach((animInfo: any) => playAnimComponentV2(animInfo));\n meshData.isAnimPlaying = true;\n console.log('[CustomMesh] Playing', allAnimComponents.length, 'animations on click for:', meshConfig.name);\n }\n }\n }\n\n // Audio - toggle on click if audioTriggerMode is 'click'\n if (audioTriggerMode === 'click' && meshConfig.interaction?.playAudio && meshData.audioSlotId) {\n const slot = entity.sound?.slot(meshData.audioSlotId);\n if (slot) {\n if (slot.isPlaying) {\n slot.pause();\n meshData.audioPlaying = false;\n console.log('[CustomMesh] Paused audio on click for:', meshConfig.name);\n } else {\n slot.play();\n meshData.audioPlaying = true;\n console.log('[CustomMesh] Playing audio on click for:', meshConfig.name);\n }\n }\n }\n\n // Direct link - trigger on click if directLinkTriggerMode is 'click'\n if (directLinkTriggerMode === 'click' && meshConfig.interaction?.triggerDirectLink) {\n handleDirectLink(meshConfig);\n }\n };\n\n // Click handler\n const meshClickHandler = (e: MouseEvent) => {\n if (performRaycast(e.clientX, e.clientY)) {\n handleClick();\n }\n };\n\n // Hover handler - handles both hover in and hover out\n const meshHoverHandler = (e: MouseEvent) => {\n const nowHovering = performRaycast(e.clientX, e.clientY);\n if (nowHovering && !isHovering) {\n isHovering = true;\n handleHoverIn();\n } else if (!nowHovering && isHovering) {\n isHovering = false;\n handleHoverOut();\n }\n };\n\n // Mouse leave handler to reset hover state\n const meshLeaveHandler = () => {\n if (isHovering) {\n isHovering = false;\n handleHoverOut();\n }\n };\n\n canvasForMesh.addEventListener('click', meshClickHandler);\n canvasForMesh.addEventListener('mousemove', meshHoverHandler);\n canvasForMesh.addEventListener('mouseleave', meshLeaveHandler);\n\n // Store handlers for cleanup\n (entity as any).meshClickHandler = meshClickHandler;\n (entity as any).meshHoverHandler = meshHoverHandler;\n (entity as any).meshLeaveHandler = meshLeaveHandler;\n }\n\n /**\n * Show mesh popup\n */\n function showMeshPopup(meshConfig: any): void {\n // Remove existing popup if any\n const existingPopup = document.querySelector('.storysplat-mesh-popup');\n if (existingPopup) existingPopup.remove();\n\n const popup = document.createElement('div');\n popup.className = 'storysplat-mesh-popup';\n popup.style.cssText = `\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background: rgba(0, 0, 0, 0.9);\n padding: 30px;\n border-radius: 12px;\n max-width: 600px;\n max-height: 80vh;\n overflow: auto;\n z-index: 100001;\n color: white;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `;\n\n const title = document.createElement('h2');\n title.textContent = meshConfig.name || 'Custom Mesh';\n title.style.cssText = 'margin-top: 0; margin-bottom: 16px;';\n popup.appendChild(title);\n\n if (meshConfig.interaction.popupContent) {\n const content = document.createElement('p');\n content.textContent = meshConfig.interaction.popupContent;\n content.style.cssText = 'margin: 0; line-height: 1.6;';\n popup.appendChild(content);\n }\n\n const closeBtn = document.createElement('button');\n closeBtn.textContent = '× Close';\n closeBtn.style.cssText = `\n position: absolute;\n top: 10px;\n right: 10px;\n background: rgba(255, 255, 255, 0.2);\n border: none;\n color: white;\n padding: 8px 16px;\n border-radius: 6px;\n cursor: pointer;\n font-size: 16px;\n `;\n closeBtn.onclick = () => popup.remove();\n popup.appendChild(closeBtn);\n\n document.body.appendChild(popup);\n }\n\n /**\n * Update mesh visibility based on current waypoint/progress\n * Called when waypoint changes or progress updates\n */\n function updateCustomMeshVisibility(): void {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n\n customMeshEntities.forEach((meshData) => {\n const { entity, config: meshConfig } = meshData;\n const range = (entity as any).visibilityRange;\n\n if (range) {\n let visible = true;\n\n if (range.type === 'waypoint') {\n // Show only between specific waypoints\n visible = waypointIndex >= range.start && waypointIndex <= range.end;\n } else if (range.type === 'percentage') {\n // Show based on scroll percentage (0-100)\n visible = scrollPercent >= range.start && scrollPercent <= range.end;\n }\n\n entity.enabled = visible;\n }\n\n // Update opacity if animated\n if (meshConfig.opacityMode === 'animated' && meshConfig.opacityAnimation) {\n const anim = meshConfig.opacityAnimation;\n\n if (scrollPercent >= anim.startPercent && scrollPercent <= anim.endPercent) {\n const progress = (scrollPercent - anim.startPercent) / (anim.endPercent - anim.startPercent);\n const opacity = anim.startOpacity + (anim.endOpacity - anim.startOpacity) * progress;\n\n // Apply opacity to all mesh instances in the entity\n applyOpacityToCustomMesh(entity, opacity);\n }\n }\n\n // Update billboard state based on range (matches HTML export lines 664-674)\n if (meshConfig.billboard && meshConfig.billboardRange) {\n const bRange = meshConfig.billboardRange;\n let billboardActive = false;\n\n if (bRange.type === 'percentage') {\n billboardActive = scrollPercent >= bRange.start && scrollPercent <= bRange.end;\n } else if (bRange.type === 'waypoint') {\n billboardActive = waypointIndex >= bRange.start && waypointIndex <= bRange.end;\n }\n\n // Store billboard state for the update loop to use\n (entity as any)._billboardActive = billboardActive;\n } else if (meshConfig.billboard) {\n // Billboard always active if no range specified\n (entity as any)._billboardActive = true;\n }\n });\n }\n\n /**\n * Apply opacity to all mesh render components\n * Uses BLEND_PREMULTIPLIED with depthWrite=true for proper GSplat depth sorting\n */\n function applyOpacityToCustomMesh(entity: pc.Entity, opacity: number): void {\n function applyToEntity(ent: pc.Entity): void {\n if ((ent as any).render && (ent as any).render.meshInstances) {\n (ent as any).render.meshInstances.forEach((meshInstance: any) => {\n if (meshInstance.material) {\n // Clone material if it's shared to avoid affecting other instances\n if (!meshInstance.material._isCloned) {\n meshInstance.material = meshInstance.material.clone();\n meshInstance.material._isCloned = true;\n }\n\n meshInstance.material.opacity = opacity;\n // Use premultiplied alpha with depth write for proper GSplat depth sorting\n meshInstance.material.blendType = pc.BLEND_PREMULTIPLIED;\n meshInstance.material.depthTest = true;\n meshInstance.material.depthWrite = true;\n meshInstance.material.alphaTest = 0.01;\n meshInstance.material.update();\n }\n });\n }\n\n // Apply to children\n ent.children.forEach((child: any) => applyToEntity(child));\n }\n\n applyToEntity(entity);\n }\n\n /**\n * Initialize all custom meshes from config\n */\n async function initCustomMeshes(): Promise<void> {\n if (!config.customMeshes || config.customMeshes.length === 0) {\n console.log('[StorySplat Viewer] No custom meshes to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.customMeshes.length} custom meshes...`);\n console.log('[StorySplat Viewer] All custom mesh configs:', config.customMeshes);\n\n // Defer mesh loading to ensure PlayCanvas is fully initialized\n // This helps avoid race conditions with the asset loader\n setTimeout(() => {\n let loadedCount = 0;\n let skippedCount = 0;\n\n config.customMeshes!.forEach((meshConfig: any, index: number) => {\n console.log(`[CustomMesh] Processing mesh ${index}:`, {\n name: meshConfig.name,\n enabled: meshConfig.enabled,\n hasModelUrl: !!meshConfig.modelUrl,\n modelUrl: meshConfig.modelUrl?.substring(0, 100) + '...'\n });\n\n if (meshConfig.enabled !== false) {\n try {\n const entity = loadCustomMesh(meshConfig, index);\n if (entity) {\n loadedCount++;\n } else {\n skippedCount++;\n console.warn(`[CustomMesh] Mesh ${meshConfig.name} returned null (likely missing modelUrl)`);\n }\n } catch (err) {\n console.error('[CustomMesh] Error loading mesh', meshConfig.name, ':', err);\n skippedCount++;\n }\n } else {\n console.log('[CustomMesh] Skipping disabled mesh:', meshConfig.name, '(enabled =', meshConfig.enabled, ')');\n skippedCount++;\n }\n });\n\n console.log(`[CustomMesh] Summary: ${loadedCount} loaded, ${skippedCount} skipped`);\n }, 100);\n\n console.log(`[StorySplat Viewer] ${config.customMeshes.length} custom meshes queued for loading`);\n }\n\n /**\n * Cleanup all custom meshes\n */\n function cleanupCustomMeshes(): void {\n const canvasForCleanup = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n customMeshEntities.forEach((meshData) => {\n const entity = meshData.entity;\n if ((entity as any).meshClickHandler) {\n canvasForCleanup.removeEventListener('click', (entity as any).meshClickHandler);\n }\n entity.destroy();\n });\n\n customMeshEntities.clear();\n }\n\n // Listen for progress updates to update mesh visibility\n events.on('progressUpdate', () => {\n updateCustomMeshVisibility();\n });\n\n // =====================================================\n // SKYBOX SYSTEM\n // =====================================================\n\n /**\n * Initialize skybox from config with IBL (Image-Based Lighting) support\n * Matches HTML export skyboxSystem.ts\n * Supports both nested (config.skybox.url) and flat (config.skyboxUrl) formats\n */\n function initSkybox(): void {\n // Support both config formats: nested object and flat properties\n const skyboxUrl = config.skybox?.url || config.skyboxUrl;\n if (!skyboxUrl) {\n console.log('[StorySplat Viewer] No skybox configured');\n return;\n }\n\n // Get rotation from either format (value is in radians)\n const skyboxRotation = config.skybox?.rotation ?? config.skyboxRotation ?? 0;\n const skyboxIntensity = config.skybox?.intensity ?? 1.0;\n const enableIBL = config.skybox?.enableIBL ?? true;\n\n console.log('[StorySplat Viewer] Creating skybox:', skyboxUrl, 'rotation:', skyboxRotation, 'rad =', skyboxRotation * (180 / Math.PI), 'deg', 'IBL:', enableIBL);\n\n // Check if URL is an HDR/EXR environment map or a regular image\n const isHDR = skyboxUrl.toLowerCase().includes('.hdr') || skyboxUrl.toLowerCase().includes('.exr');\n\n // Create a large sphere for the skybox (inverted normals) - visual display\n const skyboxEntity = new pc.Entity('skybox');\n\n // Create skybox material\n const skyboxMaterial = new pc.StandardMaterial();\n skyboxMaterial.useLighting = false;\n skyboxMaterial.cull = pc.CULLFACE_FRONT; // Render inside of sphere\n\n // Load skybox texture\n const textureAsset = new pc.Asset('skybox-texture', 'texture', { url: skyboxUrl });\n app.assets.add(textureAsset);\n\n textureAsset.ready((asset: any) => {\n const texture = asset.resource as pc.Texture;\n\n // Apply to skybox sphere material\n skyboxMaterial.emissiveMap = texture;\n skyboxMaterial.emissive = new pc.Color(skyboxIntensity, skyboxIntensity, skyboxIntensity);\n skyboxMaterial.update();\n console.log('[StorySplat Viewer] Skybox texture loaded');\n\n // Apply IBL if enabled - use the texture for environment lighting\n if (enableIBL) {\n try {\n // Set scene exposure for HDR content\n if (isHDR) {\n app.scene.exposure = skyboxIntensity;\n // Set tone mapping via rendering options (if available)\n (app.scene as any).toneMapping = pc.TONEMAP_ACES;\n console.log('[StorySplat Viewer] HDR tone mapping enabled');\n }\n\n // For IBL, we need to create environment lighting from the texture\n // PlayCanvas uses envAtlas for prefiltered environment maps\n // For a regular 2D texture, we apply ambient light color derived from it\n if (texture) {\n // Set ambient lighting based on skybox\n // Use a neutral ambient derived from skybox intensity\n app.scene.ambientLight = new pc.Color(\n 0.3 * skyboxIntensity,\n 0.3 * skyboxIntensity,\n 0.35 * skyboxIntensity\n );\n\n // If we have a cubemap (6-face or equirectangular HDR), set it as env atlas\n // For now, enhance ambient based on the skybox\n console.log('[StorySplat Viewer] IBL ambient lighting applied with intensity:', skyboxIntensity);\n }\n } catch (iblError) {\n console.warn('[StorySplat Viewer] Failed to apply IBL:', iblError);\n }\n }\n });\n\n textureAsset.on('error', (err: any) => {\n console.error('[StorySplat Viewer] Failed to load skybox texture:', err);\n });\n\n app.assets.load(textureAsset);\n\n // Add sphere component\n skyboxEntity.addComponent('render', {\n type: 'sphere',\n material: skyboxMaterial,\n castShadows: false,\n receiveShadows: false\n });\n\n // Scale to encompass scene (large sphere)\n skyboxEntity.setLocalScale(500, 500, 500);\n\n // Apply rotation\n if (skyboxRotation !== 0) {\n const rotationDegrees = skyboxRotation * (180 / Math.PI);\n skyboxEntity.setEulerAngles(0, rotationDegrees, 0);\n }\n\n // Make skybox follow camera position (so it always appears infinite)\n app.on('update', () => {\n const camPos = camera.getPosition();\n skyboxEntity.setPosition(camPos.x, camPos.y, camPos.z);\n });\n\n app.root.addChild(skyboxEntity);\n console.log('[StorySplat Viewer] Skybox created with rotation:', skyboxRotation, 'intensity:', skyboxIntensity);\n }\n\n // =====================================================\n // CUSTOM LIGHTING SYSTEM\n // =====================================================\n const lightEntities: pc.Entity[] = [];\n\n /**\n * Convert hex color to PlayCanvas Color\n */\n function hexToColorForLights(hex: string): pc.Color {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n if (result) {\n return new pc.Color(\n parseInt(result[1], 16) / 255,\n parseInt(result[2], 16) / 255,\n parseInt(result[3], 16) / 255\n );\n }\n return new pc.Color(1, 1, 1); // Default white\n }\n\n /**\n * Create point light\n */\n function createPointLight(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Point Light');\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_POINT,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n range: lightConfig.range || 10,\n castShadows: lightConfig.castShadows || false\n });\n\n // Position (negate Z for coordinate conversion)\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n entity.enabled = lightConfig.enabled !== false;\n return entity;\n }\n\n /**\n * Create directional light\n */\n function createDirectionalLightEntity(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Directional Light');\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_DIRECTIONAL,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n castShadows: lightConfig.castShadows || false\n });\n\n // Position (negate Z for coordinate conversion)\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n // Direction (rotation)\n const rot = lightConfig.rotation;\n if (rot) {\n const radToDeg = 180 / Math.PI;\n entity.setEulerAngles(\n (rot._x ?? rot.x ?? 0) * radToDeg,\n (rot._y ?? rot.y ?? 0) * radToDeg,\n (rot._z ?? rot.z ?? 0) * radToDeg\n );\n } else {\n entity.setEulerAngles(45, 0, 0);\n }\n\n entity.enabled = lightConfig.enabled !== false;\n return entity;\n }\n\n /**\n * Create hemispheric light (approximated)\n */\n function createHemisphericLight(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Hemispheric Light');\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_DIRECTIONAL,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n castShadows: false\n });\n\n // Position\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n // Point upwards for hemispheric effect\n entity.setEulerAngles(-90, 0, 0);\n\n entity.enabled = lightConfig.enabled !== false;\n\n // If ground color is specified, adjust scene ambient\n if (lightConfig.groundColor) {\n const groundColor = hexToColorForLights(lightConfig.groundColor);\n const skyColor = hexToColorForLights(lightConfig.color || '#ffffff');\n\n app.scene.ambientLight = new pc.Color(\n (skyColor.r + groundColor.r * 0.3) / 1.3,\n (skyColor.g + groundColor.g * 0.3) / 1.3,\n (skyColor.b + groundColor.b * 0.3) / 1.3\n );\n }\n\n return entity;\n }\n\n /**\n * Create ambient light\n */\n function createAmbientLight(lightConfig: any): null {\n const color = hexToColorForLights(lightConfig.color || '#404040');\n const intensity = lightConfig.intensity || 0.4;\n\n app.scene.ambientLight = new pc.Color(\n color.r * intensity,\n color.g * intensity,\n color.b * intensity\n );\n\n console.log('[StorySplat Viewer] Set ambient light:', lightConfig.name || 'Ambient Light');\n return null;\n }\n\n /**\n * Create spot light\n */\n function createSpotLight(lightConfig: any): pc.Entity {\n const entity = new pc.Entity(lightConfig.name || 'Spot Light');\n\n // Convert angles from radians to degrees if they come from the editor (stored in radians)\n // PlayCanvas expects degrees for cone angles\n const radToDeg = 180 / Math.PI;\n const angleRad = lightConfig.angle ?? (45 * Math.PI / 180); // Default to 45 degrees in radians\n const angleDeg = angleRad * radToDeg;\n\n // Inner cone angle is typically smaller than outer - use exponent to derive it\n const exponent = lightConfig.exponent ?? 2;\n const innerAngleDeg = lightConfig.innerConeAngle ?? lightConfig.innerAngle ?? (angleDeg * 0.8);\n const outerAngleDeg = lightConfig.outerConeAngle ?? lightConfig.outerAngle ?? angleDeg;\n\n entity.addComponent('light', {\n type: pc.LIGHTTYPE_SPOT,\n color: hexToColorForLights(lightConfig.color || '#ffffff'),\n intensity: lightConfig.intensity || 1,\n range: lightConfig.range || 10,\n innerConeAngle: innerAngleDeg,\n outerConeAngle: outerAngleDeg,\n castShadows: lightConfig.castShadows || false,\n shadowBias: lightConfig.shadowBias ?? 0.05,\n normalOffsetBias: lightConfig.normalOffsetBias ?? 0.05\n });\n\n // Position (negate Z for coordinate conversion)\n const pos = lightConfig.position;\n if (pos) {\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n }\n\n // Direction (rotation) - spot lights need to be pointed at their target\n const rot = lightConfig.rotation;\n if (rot) {\n const radToDeg = 180 / Math.PI;\n entity.setEulerAngles(\n (rot._x ?? rot.x ?? 0) * radToDeg,\n (rot._y ?? rot.y ?? 0) * radToDeg,\n (rot._z ?? rot.z ?? 0) * radToDeg\n );\n } else if (lightConfig.direction) {\n // Alternative: direction vector to rotation\n const dir = lightConfig.direction;\n const dirVec = new pc.Vec3(\n dir._x ?? dir.x ?? 0,\n dir._y ?? dir.y ?? -1,\n -(dir._z ?? dir.z ?? 0)\n ).normalize();\n // Calculate rotation from direction\n entity.lookAt(\n entity.getPosition().x + dirVec.x,\n entity.getPosition().y + dirVec.y,\n entity.getPosition().z + dirVec.z\n );\n } else {\n // Default: point downward\n entity.setEulerAngles(90, 0, 0);\n }\n\n entity.enabled = lightConfig.enabled !== false;\n return entity;\n }\n\n /**\n * Initialize all custom lights from config\n */\n function initCustomLights(): void {\n if (!config.lights || config.lights.length === 0) {\n console.log('[StorySplat Viewer] No custom lights to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.lights.length} custom lights...`);\n\n config.lights.forEach((lightConfig: any, index: number) => {\n let entity: pc.Entity | null = null;\n\n switch (lightConfig.type) {\n case 'point':\n entity = createPointLight(lightConfig);\n break;\n case 'directional':\n entity = createDirectionalLightEntity(lightConfig);\n break;\n case 'hemispheric':\n entity = createHemisphericLight(lightConfig);\n break;\n case 'ambient':\n createAmbientLight(lightConfig);\n break;\n case 'spot':\n entity = createSpotLight(lightConfig);\n break;\n default:\n console.warn('[StorySplat Viewer] Unknown light type:', lightConfig.type);\n return;\n }\n\n if (entity) {\n app.root.addChild(entity);\n lightEntities.push(entity);\n console.log(`[StorySplat Viewer] Created ${lightConfig.type} light:`, lightConfig.name || `Light ${index}`);\n }\n });\n\n console.log('[StorySplat Viewer] Lighting setup complete');\n }\n\n /**\n * Setup spatial audio for a hotspot using Web Audio API\n * Matches the HTML export's setupHotspotAudio function in hotspotSystem.ts\n */\n function setupHotspotAudio(entity: pc.Entity, hotspot: any): HotspotAudioElements | null {\n if (!hotspot.audioUrl) return null;\n\n const audio = document.createElement('audio');\n audio.src = hotspot.audioUrl;\n audio.loop = hotspot.audioLoop || false;\n audio.volume = hotspot.audioVolume !== undefined ? hotspot.audioVolume : 1;\n audio.crossOrigin = 'anonymous';\n\n allAudioElements.push(audio);\n\n if (hotspot.audioSpatial) {\n // Create AudioContext for spatial audio\n const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();\n allAudioContexts.push(audioCtx);\n\n const source = audioCtx.createMediaElementSource(audio);\n const panner = audioCtx.createPanner();\n\n // Apply spatial settings (matching HTML export)\n panner.panningModel = 'HRTF';\n panner.distanceModel = (hotspot.audioDistanceModel || 'linear') as DistanceModelType; // Match HTML export default\n panner.refDistance = hotspot.audioRefDistance !== undefined ? hotspot.audioRefDistance : 1;\n panner.maxDistance = hotspot.audioMaxDistance !== undefined ? hotspot.audioMaxDistance : 100;\n panner.rolloffFactor = hotspot.audioRolloffFactor !== undefined ? hotspot.audioRolloffFactor : 1;\n\n // Position audio at hotspot location\n const pos = entity.getPosition();\n panner.setPosition(pos.x, pos.y, pos.z);\n\n // Connect audio graph\n source.connect(panner);\n panner.connect(audioCtx.destination);\n\n // Update audio position each frame\n const updateAudioPosition = () => {\n if (!entity || !entity.getPosition) return;\n const hotspotPos = entity.getPosition();\n panner.setPosition(hotspotPos.x, hotspotPos.y, hotspotPos.z);\n\n // Update listener position to camera\n if (camera && camera.getPosition) {\n const camPos = camera.getPosition();\n const camForward = camera.forward;\n const camUp = camera.up;\n\n if (audioCtx.listener.positionX) {\n // Modern API\n audioCtx.listener.positionX.value = camPos.x;\n audioCtx.listener.positionY.value = camPos.y;\n audioCtx.listener.positionZ.value = camPos.z;\n audioCtx.listener.forwardX.value = camForward.x;\n audioCtx.listener.forwardY.value = camForward.y;\n audioCtx.listener.forwardZ.value = camForward.z;\n audioCtx.listener.upX.value = camUp.x;\n audioCtx.listener.upY.value = camUp.y;\n audioCtx.listener.upZ.value = camUp.z;\n } else {\n // Legacy API\n audioCtx.listener.setPosition(camPos.x, camPos.y, camPos.z);\n audioCtx.listener.setOrientation(\n camForward.x, camForward.y, camForward.z,\n camUp.x, camUp.y, camUp.z\n );\n }\n }\n };\n\n // Register update function\n app.on('update', updateAudioPosition);\n\n console.log(`[Audio] Spatial audio setup for hotspot: ${hotspot.title}, refDist=${panner.refDistance}, maxDist=${panner.maxDistance}`);\n\n return { audio, audioCtx, source, panner, updateAudioPosition };\n } else {\n // Non-spatial audio\n console.log(`[Audio] Non-spatial audio setup for hotspot: ${hotspot.title}`);\n return { audio };\n }\n }\n\n /**\n * Setup spatial audio for video hotspot using Web Audio API\n * Makes the video's audio 3D positional based on hotspot location\n */\n function setupVideoSpatialAudio(\n entity: pc.Entity,\n video: HTMLVideoElement,\n hotspot: any\n ): VideoSpatialAudio | null {\n // Check if video should have spatial audio\n if (!hotspot.videoSpatialAudio && hotspot.videoMuted !== false) {\n // No spatial audio requested and video is muted\n return null;\n }\n\n try {\n // Create AudioContext for spatial audio\n const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();\n allAudioContexts.push(audioCtx);\n\n // Create source from video element\n const source = audioCtx.createMediaElementSource(video);\n\n // Create panner for 3D positioning\n const panner = audioCtx.createPanner();\n panner.panningModel = 'HRTF';\n panner.distanceModel = (hotspot.videoDistanceModel || 'linear') as DistanceModelType; // Match HTML export default\n panner.refDistance = hotspot.videoRefDistance !== undefined ? hotspot.videoRefDistance : 1;\n panner.maxDistance = hotspot.videoMaxDistance !== undefined ? hotspot.videoMaxDistance : 100;\n panner.rolloffFactor = hotspot.videoRolloffFactor !== undefined ? hotspot.videoRolloffFactor : 1;\n\n // Position panner at hotspot location\n const pos = entity.getPosition();\n panner.setPosition(pos.x, pos.y, pos.z);\n\n // Connect: source -> panner -> destination\n source.connect(panner);\n panner.connect(audioCtx.destination);\n\n // Update panner and listener position each frame\n app.on('update', () => {\n if (!entity || !entity.getPosition) return;\n\n // Update panner position to hotspot location\n const hotspotPos = entity.getPosition();\n panner.setPosition(hotspotPos.x, hotspotPos.y, hotspotPos.z);\n\n // Update listener position to camera\n if (camera && camera.getPosition) {\n const camPos = camera.getPosition();\n const camForward = camera.forward;\n const camUp = camera.up;\n\n if (audioCtx.listener.positionX) {\n // Modern API\n audioCtx.listener.positionX.value = camPos.x;\n audioCtx.listener.positionY.value = camPos.y;\n audioCtx.listener.positionZ.value = camPos.z;\n audioCtx.listener.forwardX.value = camForward.x;\n audioCtx.listener.forwardY.value = camForward.y;\n audioCtx.listener.forwardZ.value = camForward.z;\n audioCtx.listener.upX.value = camUp.x;\n audioCtx.listener.upY.value = camUp.y;\n audioCtx.listener.upZ.value = camUp.z;\n } else {\n // Legacy API\n audioCtx.listener.setPosition(camPos.x, camPos.y, camPos.z);\n audioCtx.listener.setOrientation(\n camForward.x, camForward.y, camForward.z,\n camUp.x, camUp.y, camUp.z\n );\n }\n }\n });\n\n console.log(`[Audio] Video spatial audio setup for hotspot: ${hotspot.title}, refDist=${panner.refDistance}, maxDist=${panner.maxDistance}`);\n\n return { audioCtx, source, panner };\n } catch (err) {\n console.warn('[Audio] Failed to setup video spatial audio:', err);\n return null;\n }\n }\n\n /**\n * Setup waypoint audio for audio interactions\n * Matches the HTML export's audioSystem.ts\n */\n function setupWaypointAudio(): void {\n if (!config.waypoints || config.waypoints.length === 0) return;\n\n // Extract audio interactions from waypoints\n config.waypoints.forEach((waypoint: any, waypointIndex: number) => {\n if (waypoint.interactions && Array.isArray(waypoint.interactions)) {\n waypoint.interactions.forEach((interaction: any) => {\n if (interaction.type === 'audio' && interaction.data) {\n const data = interaction.data;\n const audioId = interaction.id;\n\n // Create audio entity\n const audioEntity = new pc.Entity(`waypoint-audio-${audioId}`);\n\n // Position at waypoint location (with Z negation for PlayCanvas)\n const wpPos = waypoint.position || { x: 0, y: 0, z: 0 };\n audioEntity.setPosition(\n wpPos._x ?? wpPos.x ?? waypoint.x ?? 0,\n wpPos._y ?? wpPos.y ?? waypoint.y ?? 1.6,\n -(wpPos._z ?? wpPos.z ?? waypoint.z ?? 0)\n );\n\n // Configure sound component\n const soundConfig: any = {\n slots: {\n [audioId]: {\n name: audioId,\n loop: data.loop || false,\n autoPlay: false, // We control playback via waypoint\n volume: data.volume !== undefined ? data.volume : 1,\n pitch: 1,\n positional: data.spatialSound || false,\n distanceModel: getPlayCanvasDistanceModel(data.distanceModel || 'exponential'),\n maxDistance: data.maxDistance || 10000,\n refDistance: data.refDistance || 1,\n rollOffFactor: data.rolloffFactor || 1\n }\n }\n };\n\n audioEntity.addComponent('sound', soundConfig);\n\n // Load audio asset\n const audioAsset = new pc.Asset(\n `waypoint-audio-asset-${audioId}`,\n 'audio',\n { url: data.url }\n );\n\n app.assets.add(audioAsset);\n\n audioAsset.ready(() => {\n const slot = audioEntity.sound?.slot(audioId);\n if (slot) {\n slot.asset = audioAsset.id;\n }\n // Mark as ready for proximity-based playback\n const audioDataRef = waypointAudioMap.get(audioId);\n if (audioDataRef) {\n audioDataRef.assetReady = true;\n }\n console.log(`[Audio] Waypoint audio loaded: ${audioId}, spatialSound=${data.spatialSound}, maxDistance=${data.maxDistance}`);\n });\n\n app.assets.load(audioAsset);\n\n // Add to scene\n app.root.addChild(audioEntity);\n\n // Store audio data\n waypointAudioMap.set(audioId, {\n entity: audioEntity,\n waypointIndex: waypointIndex,\n config: data,\n slotId: audioId,\n playing: false,\n autoplayTriggered: false,\n assetReady: false\n });\n\n console.log(`[Audio] Waypoint audio created: ${audioId} at waypoint ${waypointIndex}, spatial=${data.spatialSound}`);\n }\n });\n }\n });\n\n if (waypointAudioMap.size > 0) {\n console.log(`[StorySplat Viewer] Setup ${waypointAudioMap.size} waypoint audio sources`);\n }\n }\n\n // Map distance model string to PlayCanvas constant\n function getPlayCanvasDistanceModel(modelStr: string): string {\n switch (modelStr) {\n case 'linear':\n return 'linear';\n case 'inverse':\n return 'inverse';\n case 'exponential':\n default:\n return 'exponential';\n }\n }\n\n /**\n * Update waypoint audio based on current waypoint (for autoplay on waypoint entry)\n * Called when waypoint changes\n */\n function updateWaypointAudio(currentIndex: number, previousIndex: number): void {\n waypointAudioMap.forEach((audioData, audioId) => {\n const { entity, waypointIndex, config, slotId, autoplayTriggered } = audioData;\n const slot = entity.sound?.slot(slotId);\n if (!slot) return;\n\n // Check if we're entering this waypoint\n const isEntering = currentIndex === waypointIndex && previousIndex !== waypointIndex;\n\n // Check if we're leaving this waypoint\n const isLeaving = previousIndex === waypointIndex && currentIndex !== waypointIndex;\n\n // Handle autoplay on waypoint entry (only if autoplay is explicitly true)\n if (isEntering && config.autoplay && !autoplayTriggered) {\n console.log(`[Audio] Autoplay waypoint audio: ${audioId} at waypoint ${waypointIndex}`);\n if (!slot.isPlaying) {\n slot.play();\n audioData.playing = true;\n audioData.autoplayTriggered = true;\n }\n }\n\n // Handle stop on waypoint exit\n if (isLeaving && config.stopOnExit && audioData.playing) {\n console.log(`[Audio] Stopping waypoint audio on exit: ${audioId}`);\n if (slot.isPlaying) {\n slot.stop();\n audioData.playing = false;\n audioData.autoplayTriggered = false;\n }\n }\n });\n }\n\n /**\n * Update waypoint audio based on PROXIMITY to audio source\n * For spatial audio, plays when camera is within maxDistance of the audio source\n * Called every frame\n */\n function updateWaypointAudioProximity(): void {\n if (waypointAudioMap.size === 0) return;\n\n const cameraPos = camera.getPosition();\n\n waypointAudioMap.forEach((audioData, audioId) => {\n const { entity, config, slotId, assetReady, playing } = audioData;\n\n // Skip if asset not ready yet\n if (!assetReady) return;\n\n const slot = entity.sound?.slot(slotId);\n if (!slot) return;\n\n // For spatial audio, use proximity-based playback\n if (config.spatialSound) {\n const audioPos = entity.getPosition();\n const distance = cameraPos.distance(audioPos);\n const maxDistance = config.maxDistance || 20;\n\n // Play when entering proximity (within maxDistance)\n if (distance <= maxDistance && !playing) {\n console.log(`[Audio] Proximity play: ${audioId}, distance=${distance.toFixed(2)}, maxDistance=${maxDistance}`);\n slot.play();\n audioData.playing = true;\n }\n // Stop when leaving proximity (only if stopOnExit is true)\n else if (distance > maxDistance && playing && config.stopOnExit) {\n console.log(`[Audio] Proximity stop: ${audioId}, distance=${distance.toFixed(2)}`);\n slot.stop();\n audioData.playing = false;\n }\n }\n });\n }\n\n // Track which waypoints are \"active\" (camera within trigger distance)\n const activeWaypointTriggers = new Set<number>();\n\n /**\n * Check waypoint trigger distances and execute/reverse interactions\n * Called every frame in the update loop\n */\n function checkWaypointTriggerDistance(): void {\n if (!config.waypoints?.length) return;\n\n const cameraPos = camera.getPosition();\n\n config.waypoints.forEach((wp: any, index: number) => {\n const wpPos = new pc.Vec3(\n wp.position?.x ?? 0,\n wp.position?.y ?? 0,\n -(wp.position?.z ?? 0) // Z-axis negation for PlayCanvas\n );\n\n const distance = cameraPos.distance(wpPos);\n const triggerDist = wp.triggerDistance ?? 1.0;\n\n if (distance <= triggerDist) {\n // Entering trigger zone\n if (!activeWaypointTriggers.has(index)) {\n activeWaypointTriggers.add(index);\n console.log(`[StorySplat] Waypoint ${index} triggered (distance: ${distance.toFixed(2)}, threshold: ${triggerDist})`);\n executeWaypointInteractions(wp, index);\n }\n } else {\n // Exiting trigger zone\n if (activeWaypointTriggers.has(index)) {\n activeWaypointTriggers.delete(index);\n console.log(`[StorySplat] Waypoint ${index} exited`);\n reverseWaypointInteractions(wp, index);\n }\n }\n });\n }\n\n /**\n * Execute all interactions on a waypoint when entering trigger distance\n */\n function executeWaypointInteractions(waypoint: any, waypointIndex: number): void {\n if (!waypoint.interactions?.length) return;\n\n waypoint.interactions.forEach((interaction: any) => {\n if (interaction.type === 'audio') {\n // The audio ID matches interaction.id (set in setupWaypointAudio)\n const audioId = interaction.id;\n const audioData = waypointAudioMap.get(audioId);\n if (audioData && audioData.assetReady && !audioData.playing) {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n slot.play();\n audioData.playing = true;\n }\n }\n }\n // Info interactions would show popup (if implemented)\n });\n }\n\n /**\n * Reverse/stop interactions when exiting trigger distance\n */\n function reverseWaypointInteractions(waypoint: any, waypointIndex: number): void {\n if (!waypoint.interactions?.length) return;\n\n waypoint.interactions.forEach((interaction: any) => {\n if (interaction.type === 'audio') {\n const stopOnExit = interaction.data?.stopOnExit ?? false;\n if (stopOnExit) {\n // The audio ID matches interaction.id (set in setupWaypointAudio)\n const audioId = interaction.id;\n const audioData = waypointAudioMap.get(audioId);\n if (audioData && audioData.playing) {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n slot.stop();\n audioData.playing = false;\n }\n }\n }\n }\n // Info interactions would hide popup (if implemented)\n });\n }\n\n /**\n * Global mute/unmute functions\n */\n function muteAllAudio(): void {\n if (globalMuted) return;\n\n // Mute hotspot audio elements\n allAudioElements.forEach(audio => {\n storedVolumes.set(audio, audio.volume);\n audio.volume = 0;\n });\n\n // Mute waypoint audio\n waypointAudioMap.forEach((audioData) => {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n (slot as any)._storedVolume = slot.volume;\n slot.volume = 0;\n }\n });\n\n // Also mute any HTML audio/video elements\n document.querySelectorAll('audio, video').forEach((el) => {\n (el as HTMLMediaElement).muted = true;\n });\n\n globalMuted = true;\n console.log('[Audio] All audio muted');\n }\n\n function unmuteAllAudio(): void {\n if (!globalMuted) return;\n\n // Unmute hotspot audio elements\n allAudioElements.forEach(audio => {\n const storedVol = storedVolumes.get(audio);\n audio.volume = storedVol !== undefined ? storedVol : 1;\n });\n\n // Unmute waypoint audio\n waypointAudioMap.forEach((audioData) => {\n const slot = audioData.entity.sound?.slot(audioData.slotId);\n if (slot) {\n const storedVol = (slot as any)._storedVolume;\n slot.volume = storedVol !== undefined ? storedVol : 1;\n }\n });\n\n // Also unmute any HTML audio/video elements\n document.querySelectorAll('audio, video').forEach((el) => {\n (el as HTMLMediaElement).muted = false;\n });\n\n globalMuted = false;\n console.log('[Audio] All audio unmuted');\n }\n\n function toggleMuteAll(): boolean {\n if (globalMuted) {\n unmuteAllAudio();\n } else {\n muteAllAudio();\n }\n return globalMuted;\n }\n\n // Track animated GIF textures for cleanup\n const animatedGifs: AnimatedGifTexture[] = [];\n\n // Create hotspot material for images - entity is hidden until texture loads\n // Supports both static images and animated GIFs with transparency\n function createImageMaterial(\n imageUrl: string,\n useLighting: boolean = false,\n opacity: number = 1,\n onTextureReady?: () => void\n ): pc.StandardMaterial {\n const material = new pc.StandardMaterial();\n\n // Configure material for transparency with proper depth handling\n // Use premultiplied alpha blending with depth write for better GSplat compatibility\n material.blendType = pc.BLEND_PREMULTIPLIED;\n material.depthTest = true; // Enable depth testing so hotspots are occluded by splats\n material.depthWrite = true; // Enable depth write for proper GSplat sorting\n material.cull = pc.CULLFACE_NONE; // Double-sided rendering\n material.twoSidedLighting = true; // Proper lighting on both sides\n material.alphaTest = 0.01; // Discard nearly transparent pixels for better sorting\n\n // For unlit mode: disable all lighting calculations, use only emissive\n if (!useLighting) {\n // Disable diffuse lighting entirely\n material.diffuse = new pc.Color(0, 0, 0);\n // Full white emissive - texture will provide color\n material.emissive = new pc.Color(1, 1, 1);\n // Disable specular\n material.specular = new pc.Color(0, 0, 0);\n material.useLighting = false;\n } else {\n material.diffuse = new pc.Color(1, 1, 1);\n }\n\n material.opacity = opacity;\n material.update();\n\n // Check if this is a GIF - use AnimatedGifTexture for proper transparency and animation\n if (isGifUrl(imageUrl)) {\n console.log(`[Hotspot] Loading animated GIF: ${imageUrl}`);\n\n const gifTexture = new AnimatedGifTexture(app, imageUrl, {\n autoPlay: true,\n onReady: () => {\n if (isDestroyed) {\n console.log(`[Hotspot] Ignoring GIF load - viewer was destroyed: ${imageUrl}`);\n gifTexture.destroy();\n return;\n }\n\n if (gifTexture.texture) {\n // Assign texture to material based on lighting mode\n if (!useLighting) {\n material.emissiveMap = gifTexture.texture;\n material.opacityMap = gifTexture.texture;\n } else {\n material.diffuseMap = gifTexture.texture;\n material.opacityMap = gifTexture.texture;\n }\n material.update();\n\n console.log(`[Hotspot] GIF texture loaded: ${imageUrl}, useLighting=${useLighting}`);\n\n if (onTextureReady) {\n onTextureReady();\n }\n }\n },\n onError: (error) => {\n console.error(`[Hotspot] Failed to load GIF: ${imageUrl}`, error);\n // On error, make it slightly visible so user knows something is there\n material.opacity = 0.3;\n material.emissive = new pc.Color(1, 0, 0); // Red to indicate error\n material.update();\n if (onTextureReady) {\n onTextureReady(); // Still call callback so entity becomes visible\n }\n }\n });\n\n // Track for cleanup\n animatedGifs.push(gifTexture);\n } else {\n // Standard image loading\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n // Check if viewer was destroyed while image was loading (React StrictMode race condition)\n if (isDestroyed) {\n console.log(`[Hotspot] Ignoring texture load - viewer was destroyed: ${imageUrl}`);\n return;\n }\n\n // Create texture with the app's graphics device\n const texture = new pc.Texture(app.graphicsDevice, {\n width: img.width,\n height: img.height,\n format: pc.PIXELFORMAT_RGBA8,\n mipmaps: true,\n minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n\n // Upload the image data\n texture.setSource(img);\n\n // Assign texture to material based on lighting mode\n if (!useLighting) {\n // Unlit: only use emissive map (no diffuse lighting)\n material.emissiveMap = texture;\n material.opacityMap = texture;\n } else {\n // Lit: use diffuse map\n material.diffuseMap = texture;\n material.opacityMap = texture;\n }\n\n material.update();\n\n console.log(`[Hotspot] Texture loaded for: ${imageUrl}, useLighting=${useLighting}`);\n\n if (onTextureReady) {\n onTextureReady();\n }\n };\n img.onerror = (err) => {\n console.error(`[Hotspot] Failed to load texture: ${imageUrl}`, err);\n // On error, make it slightly visible so user knows something is there\n material.opacity = 0.3;\n material.emissive = new pc.Color(1, 0, 0); // Red to indicate error\n material.update();\n if (onTextureReady) {\n onTextureReady(); // Still call callback so entity becomes visible\n }\n };\n img.src = imageUrl;\n }\n\n return material;\n }\n\n // Create all hotspots\n function createHotspots(): void {\n if (!config.hotspots || config.hotspots.length === 0) {\n console.log('[StorySplat Viewer] No hotspots to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.hotspots.length} hotspots...`);\n\n config.hotspots.forEach((hotspot: any, index: number) => {\n const entity = new pc.Entity(`hotspot-${index}`);\n\n // Get position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n const pos = hotspot.position || { _x: 0, _y: 0, _z: 0 };\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n\n // Get scale\n const scale = hotspot.scale || { _x: 1, _y: 1, _z: 1 };\n const sx = Math.abs(scale._x ?? scale.x ?? 1);\n const sy = Math.abs(scale._y ?? scale.y ?? 1);\n const sz = scale._z ?? scale.z ?? 1;\n\n // Get rotation (convert from radians to degrees + 180° Y offset for BabylonJS -> PlayCanvas)\n const rot = hotspot.rotation || { _x: 0, _y: 0, _z: 0 };\n const radToDeg = 180 / Math.PI;\n const rotX = (rot._x ?? rot.x ?? 0) * radToDeg;\n const rotY = ((rot._y ?? rot.y ?? 0) * radToDeg) + 180;\n const rotZ = (rot._z ?? rot.z ?? 0) * radToDeg;\n entity.setEulerAngles(rotX, rotY, rotZ);\n\n // Create geometry based on type\n if (hotspot.type === 'sphere') {\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(hotspot.color || '#ffffff');\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // For spheres: use full opacity with depth write for proper GSplat occlusion\n // GSplat doesn't sort properly with transparent objects, so make spheres opaque-ish\n const sphereOpacity = hotspot.opacity ?? 0.8;\n if (sphereOpacity >= 0.95) {\n // Fully opaque - use standard opaque rendering\n material.opacity = 1;\n material.blendType = pc.BLEND_NONE;\n material.depthTest = true;\n material.depthWrite = true;\n } else {\n // Semi-transparent - use additive blending for better GSplat compatibility\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true; // Write depth to help with sorting\n }\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2); // Scale down spheres\n } else if (hotspot.type === 'image' && hotspot.imageUrl) {\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n // Get initial opacity (use startOpacity for animated, opacity for static)\n // Default to 1 (fully visible) - user must explicitly set startOpacity to 0 for fade-in\n let initialOpacity = 1;\n if (hotspot.opacityMode === 'animated' && hotspot.opacityAnimation) {\n // Use startOpacity if defined, otherwise default to 1 (visible)\n initialOpacity = hotspot.opacityAnimation.startOpacity !== undefined\n ? hotspot.opacityAnimation.startOpacity\n : 1;\n } else if (hotspot.opacity !== undefined) {\n initialOpacity = hotspot.opacity;\n }\n\n // Store target opacity for animation system\n (entity as any).targetOpacity = initialOpacity;\n (entity as any).textureLoaded = false;\n\n // HIDE entity until texture is loaded to prevent WebGL errors\n (entity as any).hiddenUntilTextureLoaded = true;\n entity.enabled = false;\n\n // Check useLighting setting from hotspot (default to false = unlit)\n const useLighting = hotspot.useLighting === true;\n\n const material = createImageMaterial(hotspot.imageUrl, useLighting, initialOpacity, () => {\n // Callback when texture is ready - show the entity\n (entity as any).textureLoaded = true;\n // Only enable if not hidden by visibility range\n if (!(entity as any).visibilityRange || (entity as any).shouldBeVisible) {\n entity.enabled = true;\n }\n (entity as any).hiddenUntilTextureLoaded = false;\n });\n entity.render!.material = material;\n entity.setLocalScale(sx, sy, sz);\n // Rotate plane to face forward - compensate for 180° Y offset in euler angles\n entity.rotateLocal(90, 180, 0);\n\n // Store material reference for opacity animation\n (entity as any).hotspotMaterial = material;\n\n console.log(`[Hotspot] Created image hotspot: ${hotspot.title}, opacity=${initialOpacity}, opacityMode=${hotspot.opacityMode}, useLighting=${useLighting}`);\n } else if (hotspot.type === 'video' && hotspot.videoUrl) {\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n // Detect iOS for alpha video method\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);\n\n // Determine which video URLs to use (iOS alpha support)\n const useAlphaMethod = (isIOS && hotspot.useIOSVideoAlphaMethod) || hotspot.forceIOSVideoAlphaMethodForAllDevices;\n const mainVideoUrl = useAlphaMethod && hotspot.iosMainVideoUrl ? hotspot.iosMainVideoUrl : hotspot.videoUrl;\n const alphaVideoUrl = useAlphaMethod ? (hotspot.alphaMaskVideoUrl || null) : null;\n\n // Detect if URL is a WebM file (which may contain alpha)\n const isWebMUrl = (url: string): boolean => {\n const lower = url.toLowerCase();\n return lower.endsWith('.webm') || lower.includes('format=webm') || lower.includes('video/webm');\n };\n\n // Check if this video is WebM (may have embedded alpha)\n const mainIsWebM = isWebMUrl(mainVideoUrl);\n const hasWebMAlpha = mainIsWebM && (hotspot.webmHasAlpha !== false); // Default true for WebM\n\n // Helper to create video element and texture\n const createVideoAndTexture = (url: string, isAlpha: boolean, useRGBA: boolean = false) => {\n const v = document.createElement('video');\n v.src = url;\n v.loop = hotspot.videoLoop !== false;\n v.crossOrigin = 'anonymous';\n v.playsInline = true;\n\n // Default muted state\n if (isAlpha) {\n v.muted = true; // Alpha video always muted\n } else {\n v.muted = hotspot.videoMuted !== false;\n }\n\n // Autoplay handling\n if (hotspot.mediaTriggerMode === 'autoplay') {\n v.autoplay = true;\n v.muted = true; // Autoplay requires muted (browser policy)\n }\n\n // Use RGBA format for WebM (alpha support) or standard RGB\n const t = new pc.Texture(app.graphicsDevice, {\n format: useRGBA ? pc.PIXELFORMAT_R8_G8_B8_A8 : pc.PIXELFORMAT_R8_G8_B8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n t.setSource(v);\n return { video: v, texture: t };\n };\n\n // Create main video and texture - use RGBA for WebM to support embedded alpha\n const main = createVideoAndTexture(mainVideoUrl, false, hasWebMAlpha);\n const video = main.video;\n const videoTexture = main.texture;\n\n // Create material - matching HTML export exactly\n const material = new pc.StandardMaterial();\n\n // Always use emissive for self-illumination (matches HTML export)\n material.diffuseMap = videoTexture;\n material.emissiveMap = videoTexture;\n material.emissive = new pc.Color(1, 1, 1);\n\n // Enable depth testing so hotspots are occluded by splats\n // Use depthWrite=true with premultiplied alpha for proper GSplat depth sorting\n material.depthTest = true;\n material.depthWrite = true;\n material.cull = pc.CULLFACE_NONE; // Double-sided rendering\n material.twoSidedLighting = true; // Proper lighting on both sides\n // Default blend type for non-transparent videos\n material.blendType = pc.BLEND_PREMULTIPLIED;\n material.alphaTest = 0.01;\n\n // WebM with embedded alpha - configure for transparency\n if (hasWebMAlpha) {\n material.opacityMap = videoTexture; // Alpha channel from same texture\n material.blendType = pc.BLEND_PREMULTIPLIED;\n material.alphaTest = 0.01;\n console.log(`[Hotspot] WebM video with alpha enabled: ${hotspot.title}`);\n }\n\n // Alpha video support (iOS dual-video method) - takes precedence over WebM embedded alpha\n // iOS uses a separate grayscale video where luminance = alpha (chroma key technique)\n let alphaVideo: HTMLVideoElement | null = null;\n let alphaTexture: pc.Texture | null = null;\n\n if (alphaVideoUrl) {\n const alpha = createVideoAndTexture(alphaVideoUrl, true, false);\n alphaVideo = alpha.video;\n alphaTexture = alpha.texture;\n\n material.opacityMap = alphaTexture;\n // CRITICAL: Use 'r' channel for opacity since iOS alpha videos store\n // alpha as grayscale luminance in RGB, not in actual alpha channel\n // This matches HTML export's getAlphaFromRGB = true behavior\n material.opacityMapChannel = 'r';\n material.blendType = pc.BLEND_PREMULTIPLIED;\n material.alphaTest = 0.01;\n\n // Synchronize alpha video with main video\n video.addEventListener('play', () => {\n if (alphaVideo && alphaVideo.paused) {\n alphaVideo.currentTime = video.currentTime;\n alphaVideo.play().catch(console.warn);\n }\n });\n video.addEventListener('pause', () => {\n if (alphaVideo && !alphaVideo.paused) {\n alphaVideo.pause();\n }\n });\n video.addEventListener('seeked', () => {\n if (alphaVideo) {\n alphaVideo.currentTime = video.currentTime;\n }\n });\n\n console.log(`[Hotspot] iOS alpha mask video enabled: ${hotspot.title}, alphaUrl=${alphaVideoUrl.substring(0, 50)}...`);\n }\n\n material.update();\n\n // Update textures each frame\n app.on('update', () => {\n if (video.readyState === video.HAVE_ENOUGH_DATA) {\n videoTexture.upload();\n }\n if (alphaVideo && alphaTexture && alphaVideo.readyState === alphaVideo.HAVE_ENOUGH_DATA) {\n alphaTexture.upload();\n }\n });\n\n // Aspect ratio scaling from video metadata\n const initialScale = { x: sx, y: sy, z: sz };\n video.addEventListener('loadedmetadata', () => {\n const width = video.videoWidth;\n const height = video.videoHeight;\n if (width > 0 && height > 0) {\n const ratio = width / height;\n // Only auto-adjust if scale is default (1,1)\n if (initialScale.x === 1 && initialScale.y === 1) {\n entity.setLocalScale(ratio * initialScale.y, initialScale.y, initialScale.z);\n }\n }\n });\n\n entity.render!.material = material;\n entity.setLocalScale(sx, sy, sz);\n // Rotate plane to face forward - compensate for 180° Y offset in euler angles\n entity.rotateLocal(90, 180, 0);\n\n // Store video references for playback control\n (entity as any).videoElement = video;\n (entity as any).alphaVideoElement = alphaVideo;\n (entity as any).hotspotMaterial = material;\n\n // Store media trigger mode and proximity settings\n (entity as any).mediaTriggerMode = hotspot.mediaTriggerMode || 'click';\n (entity as any).proximityDistance = hotspot.proximityDistance || 5;\n (entity as any).isVideoPlaying = false;\n\n // Setup spatial audio for video if not muted\n if (hotspot.videoMuted !== true) {\n const videoSpatialAudio = setupVideoSpatialAudio(entity, video, hotspot);\n if (videoSpatialAudio) {\n (entity as any).videoSpatialAudio = videoSpatialAudio;\n }\n }\n\n console.log(`[Hotspot] Created video hotspot: ${hotspot.title}, mode=${hotspot.mediaTriggerMode}, useAlpha=${!!alphaVideoUrl}, spatialAudio=${!!(entity as any).videoSpatialAudio}`);\n } else if (hotspot.type === 'gif' && hotspot.gifUrl) {\n // GIF hotspot implementation - animated texture using canvas\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n // Get initial opacity\n let initialOpacity = 1;\n if (hotspot.opacityMode === 'animated' && hotspot.opacityAnimation) {\n initialOpacity = hotspot.opacityAnimation.startOpacity !== undefined\n ? hotspot.opacityAnimation.startOpacity\n : 1;\n } else if (hotspot.opacity !== undefined) {\n initialOpacity = hotspot.opacity;\n }\n\n // Store state for GIF animation\n (entity as any).targetOpacity = initialOpacity;\n (entity as any).textureLoaded = false;\n (entity as any).hiddenUntilTextureLoaded = true;\n entity.enabled = false;\n\n // Check useLighting setting from hotspot (default to false = unlit)\n const useLighting = hotspot.useLighting === true;\n\n // Create material for GIF\n // Use BLEND_PREMULTIPLIED with depthWrite=true for proper GSplat depth sorting\n const material = new pc.StandardMaterial();\n material.blendType = pc.BLEND_PREMULTIPLIED;\n material.opacity = initialOpacity;\n material.depthTest = true;\n material.depthWrite = true;\n material.cull = pc.CULLFACE_NONE;\n material.twoSidedLighting = true;\n material.alphaTest = 0.01;\n\n if (!useLighting) {\n material.emissive = new pc.Color(1, 1, 1);\n material.diffuse = new pc.Color(0, 0, 0);\n }\n\n // Load GIF using SuperGif library pattern (parse frames)\n const loadAnimatedGif = async (gifUrl: string) => {\n try {\n // Create a canvas for GIF rendering\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d')!;\n\n // Load GIF as image first to get dimensions\n const img = new Image();\n img.crossOrigin = 'anonymous';\n\n img.onload = () => {\n canvas.width = img.width || 256;\n canvas.height = img.height || 256;\n\n // Draw static frame initially\n ctx.drawImage(img, 0, 0);\n\n // Create texture from canvas\n const texture = new pc.Texture(app.graphicsDevice, {\n format: pc.PIXELFORMAT_R8_G8_B8_A8,\n mipmaps: false,\n minFilter: pc.FILTER_LINEAR,\n magFilter: pc.FILTER_LINEAR,\n addressU: pc.ADDRESS_CLAMP_TO_EDGE,\n addressV: pc.ADDRESS_CLAMP_TO_EDGE\n });\n texture.setSource(canvas);\n\n material.diffuseMap = texture;\n if (!useLighting) {\n material.emissiveMap = texture;\n }\n material.opacityMap = texture;\n material.alphaTest = 0.01;\n material.update();\n\n entity.render!.material = material;\n\n // Calculate aspect ratio\n const ratio = canvas.width / canvas.height;\n if (sx === 1 && sy === 1) {\n entity.setLocalScale(ratio, 1, sz);\n } else {\n entity.setLocalScale(sx, sy, sz);\n }\n\n // Rotate plane to face forward\n entity.rotateLocal(90, 180, 0);\n\n // Mark texture as loaded\n (entity as any).textureLoaded = true;\n (entity as any).gifCanvas = canvas;\n (entity as any).gifTexture = texture;\n (entity as any).hotspotMaterial = material;\n\n // Enable entity if not hidden by visibility range\n if (!(entity as any).visibilityRange || (entity as any).shouldBeVisible) {\n entity.enabled = true;\n }\n (entity as any).hiddenUntilTextureLoaded = false;\n\n // Try to load and animate the GIF frames using fetch + gif parsing\n fetch(gifUrl)\n .then(response => response.arrayBuffer())\n .then(buffer => {\n // Simple GIF frame extraction (will show animated if browser supports it via img tag)\n // For full animation support, we continuously redraw the img element\n let frameIndex = 0;\n const animateGif = () => {\n if (!entity.enabled || !(entity as any).gifTexture) return;\n\n // Redraw from the img which browsers animate automatically\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(img, 0, 0);\n (entity as any).gifTexture.upload();\n\n // Continue animation loop\n requestAnimationFrame(animateGif);\n };\n\n // Start animation loop\n (entity as any).gifAnimationFrame = requestAnimationFrame(animateGif);\n })\n .catch(err => {\n console.warn('[Hotspot] GIF animation load failed, using static frame:', err);\n });\n\n console.log(`[Hotspot] Created GIF hotspot: ${hotspot.title}, opacity=${initialOpacity}, useLighting=${useLighting}`);\n };\n\n img.onerror = (err) => {\n console.error('[Hotspot] Failed to load GIF:', hotspot.gifUrl, err);\n // Fallback to sphere\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n const fallbackMaterial = new pc.StandardMaterial();\n fallbackMaterial.diffuse = parseColor(hotspot.color || '#FF00FF');\n fallbackMaterial.opacity = 0.8;\n fallbackMaterial.blendType = pc.BLEND_NORMAL;\n fallbackMaterial.update();\n entity.render!.material = fallbackMaterial;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n entity.enabled = true;\n (entity as any).hiddenUntilTextureLoaded = false;\n };\n\n img.src = gifUrl;\n } catch (err) {\n console.error('[Hotspot] GIF hotspot creation failed:', err);\n }\n };\n\n loadAnimatedGif(hotspot.gifUrl);\n\n } else {\n // Default: sphere\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(hotspot.color || '#4CAF50');\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // Use additive blending for better GSplat compatibility\n const sphereOpacity = hotspot.opacity ?? 0.8;\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true;\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n }\n\n // Add collision for raycasting\n entity.addComponent('collision', {\n type: hotspot.type === 'sphere' ? 'sphere' : 'box',\n radius: 0.1,\n halfExtents: new pc.Vec3(0.5, 0.5, 0.05)\n });\n\n // Store hotspot data for click handling\n (entity as any).hotspotData = hotspot;\n\n // Setup hotspot audio (spatial or non-spatial)\n const audioElements = setupHotspotAudio(entity, hotspot);\n if (audioElements) {\n (entity as any).audioElements = audioElements;\n console.log(`[StorySplat Viewer] Audio setup for hotspot: ${hotspot.title || 'Untitled'}`);\n }\n\n // Billboard mode - make plane always face the camera (with optional range support)\n if (hotspot.billboard) {\n // Store original rotation for when billboard is disabled\n const originalRotation = entity.getEulerAngles().clone();\n\n // Check if billboard range is specified\n const hasBillboardRange = hotspot.billboardRangeStart !== undefined || hotspot.billboardRangeEnd !== undefined;\n\n // Initialize billboard active state\n (entity as any)._billboardActive = !hasBillboardRange; // Active by default if no range\n (entity as any)._billboardOriginalRotation = originalRotation;\n\n app.on('update', () => {\n // Only apply billboard if active (no range or within range)\n if ((entity as any)._billboardActive) {\n entity.lookAt(camera.getPosition());\n // Rotate plane to face camera: 90° X to orient plane face forward, 180° Y to flip towards camera\n entity.rotateLocal(90, 180, 0);\n }\n });\n }\n\n // Visibility range\n if (hotspot.visibilityRange) {\n (entity as any).visibilityRange = hotspot.visibilityRange;\n entity.enabled = false; // Start hidden\n }\n\n app.root.addChild(entity);\n hotspotEntities.push(entity);\n\n console.log(`[StorySplat Viewer] Created hotspot: ${hotspot.title || 'Untitled'}`);\n });\n }\n\n // Update hotspot visibility, opacity, and video playback based on progress\n function updateHotspotVisibility(): void {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n const cameraPos = camera.getPosition();\n\n hotspotEntities.forEach((entity: any) => {\n const hotspot = entity.hotspotData;\n if (!hotspot) return;\n\n // Determine if hotspot should be visible based on visibility range\n let shouldBeVisible = true;\n const range = entity.visibilityRange;\n if (range) {\n if (range.type === 'waypoint') {\n shouldBeVisible = waypointIndex >= range.start && waypointIndex <= range.end;\n } else if (range.type === 'percentage') {\n shouldBeVisible = scrollPercent >= range.start && scrollPercent <= range.end;\n }\n }\n\n // Store visibility state for texture load callback\n entity.shouldBeVisible = shouldBeVisible;\n\n // Update billboard state based on range (if billboard mode is enabled with range)\n if (hotspot.billboard && (hotspot.billboardRangeStart !== undefined || hotspot.billboardRangeEnd !== undefined)) {\n const rangeStart = hotspot.billboardRangeStart ?? 0;\n const rangeEnd = hotspot.billboardRangeEnd ?? 100;\n const billboardActive = scrollPercent >= rangeStart && scrollPercent <= rangeEnd;\n\n // Update billboard active state\n entity._billboardActive = billboardActive;\n\n // If billboard is deactivated and we have original rotation, restore it\n if (!billboardActive && entity._billboardOriginalRotation) {\n const origRot = entity._billboardOriginalRotation;\n entity.setEulerAngles(origRot.x, origRot.y, origRot.z);\n }\n }\n\n // Only enable if: should be visible AND (not an image OR texture is loaded)\n if (entity.hiddenUntilTextureLoaded) {\n // Image hotspot waiting for texture - keep hidden\n entity.enabled = false;\n } else {\n entity.enabled = shouldBeVisible;\n }\n\n // Handle video playback triggers\n if (entity.videoElement && hotspot.type === 'video') {\n const triggerMode = entity.mediaTriggerMode || 'click';\n\n // Proximity trigger: play when camera is within proximityDistance\n if (triggerMode === 'proximity') {\n const hotspotPos = entity.getPosition();\n const distance = cameraPos.distance(hotspotPos);\n const proximityDistance = entity.proximityDistance || 5;\n\n if (distance <= proximityDistance && !entity.isVideoPlaying) {\n // Enter proximity - play video\n playVideoHotspot(entity, hotspot);\n console.log(`[Hotspot] Proximity play: ${hotspot.title}, distance=${distance.toFixed(2)}`);\n } else if (distance > proximityDistance && entity.isVideoPlaying) {\n // Leave proximity - pause video\n pauseVideoHotspot(entity);\n console.log(`[Hotspot] Proximity pause: ${hotspot.title}, distance=${distance.toFixed(2)}`);\n }\n }\n\n // Scroll trigger: play when visible based on visibility range\n if (triggerMode === 'scroll') {\n if (shouldBeVisible && !entity.isVideoPlaying) {\n // Entered visibility range - play video\n playVideoHotspot(entity, hotspot);\n console.log(`[Hotspot] Scroll play: ${hotspot.title}, scroll=${scrollPercent.toFixed(1)}%`);\n } else if (!shouldBeVisible && entity.isVideoPlaying) {\n // Left visibility range - pause video\n pauseVideoHotspot(entity);\n console.log(`[Hotspot] Scroll pause: ${hotspot.title}, scroll=${scrollPercent.toFixed(1)}%`);\n }\n }\n }\n\n // Handle opacity animation (only if texture is loaded for image hotspots)\n if (hotspot.opacityMode === 'animated' && hotspot.opacityAnimation) {\n // Skip if this is an image hotspot and texture isn't loaded yet\n if (hotspot.type === 'image' && !entity.textureLoaded) {\n return;\n }\n\n const anim = hotspot.opacityAnimation;\n const startPercent = anim.startPercent ?? 0;\n const endPercent = anim.endPercent ?? 100;\n // Default to 1 (visible) if not explicitly set\n const startOpacity = anim.startOpacity !== undefined ? anim.startOpacity : 1;\n const endOpacity = anim.endOpacity !== undefined ? anim.endOpacity : 1;\n\n let opacity: number;\n if (scrollPercent <= startPercent) {\n opacity = startOpacity;\n } else if (scrollPercent >= endPercent) {\n opacity = endOpacity;\n } else {\n // Linear interpolation\n const animRange = endPercent - startPercent;\n const progress = (scrollPercent - startPercent) / animRange;\n opacity = startOpacity + (endOpacity - startOpacity) * progress;\n }\n\n opacity = Math.max(0, Math.min(1, opacity));\n\n // Update material opacity\n if (entity.hotspotMaterial) {\n entity.hotspotMaterial.opacity = opacity;\n entity.hotspotMaterial.update();\n } else if (entity.render && entity.render.material) {\n // Fallback to render material\n (entity.render.material as any).opacity = opacity;\n (entity.render.material as any).update();\n }\n }\n });\n }\n\n // Helper to play/pause video (defined here so updateHotspotVisibility can use them)\n function playVideoHotspot(entity: any, hotspot: any): void {\n const video = entity.videoElement as HTMLVideoElement;\n const alphaVideo = entity.alphaVideoElement as HTMLVideoElement | null;\n\n if (!video) return;\n\n // Set muted state based on hotspot settings (except for autoplay which is always muted initially)\n if (entity.mediaTriggerMode !== 'autoplay') {\n video.muted = hotspot.videoMuted !== false;\n }\n\n // Resume AudioContext for spatial audio (browser requires user interaction)\n if (entity.videoSpatialAudio && entity.videoSpatialAudio.audioCtx) {\n const audioCtx = entity.videoSpatialAudio.audioCtx as AudioContext;\n if (audioCtx.state === 'suspended') {\n audioCtx.resume().then(() => {\n console.log('[Audio] Video spatial audio context resumed');\n }).catch(err => console.warn('[Audio] Failed to resume video audio context:', err));\n }\n }\n\n video.play().catch(err => console.warn('Video play failed:', err));\n if (alphaVideo) {\n alphaVideo.play().catch(err => console.warn('Alpha video play failed:', err));\n }\n entity.isVideoPlaying = true;\n }\n\n function pauseVideoHotspot(entity: any): void {\n const video = entity.videoElement as HTMLVideoElement;\n const alphaVideo = entity.alphaVideoElement as HTMLVideoElement | null;\n\n if (!video) return;\n\n video.pause();\n if (alphaVideo) {\n alphaVideo.pause();\n }\n entity.isVideoPlaying = false;\n }\n\n // Listen for progress updates to update hotspot visibility\n events.on('progressUpdate', () => {\n updateHotspotVisibility();\n });\n\n // Also call initially after hotspots are created\n setTimeout(() => {\n updateHotspotVisibility();\n }, 100);\n\n // =====================================================\n // PORTAL CREATION AND RENDERING\n // =====================================================\n\n // Create all portals (similar to hotspots but for scene-to-scene navigation)\n function createPortals(): void {\n if (!config.portals || config.portals.length === 0) {\n console.log('[StorySplat Viewer] No portals to create');\n return;\n }\n\n console.log(`[StorySplat Viewer] Creating ${config.portals.length} portals...`);\n\n config.portals.forEach((portal: any, index: number) => {\n const entity = new pc.Entity(`portal-${index}`);\n\n // Get position (negate Z for BabylonJS -> PlayCanvas coordinate conversion)\n const pos = portal.position || { _x: 0, _y: 0, _z: 0 };\n entity.setPosition(\n pos._x ?? pos.x ?? 0,\n pos._y ?? pos.y ?? 0,\n -(pos._z ?? pos.z ?? 0)\n );\n\n // Get scale\n const scale = portal.scale || { _x: 1, _y: 1, _z: 1 };\n const sx = Math.abs(scale._x ?? scale.x ?? 1);\n const sy = Math.abs(scale._y ?? scale.y ?? 1);\n const sz = scale._z ?? scale.z ?? 1;\n\n // Get rotation (convert from radians to degrees + 180° Y offset for BabylonJS -> PlayCanvas)\n const rot = portal.rotation || { _x: 0, _y: 0, _z: 0 };\n const radToDeg = 180 / Math.PI;\n const rotX = (rot._x ?? rot.x ?? 0) * radToDeg;\n const rotY = ((rot._y ?? rot.y ?? 0) * radToDeg) + 180;\n const rotZ = (rot._z ?? rot.z ?? 0) * radToDeg;\n entity.setEulerAngles(rotX, rotY, rotZ);\n\n // Create geometry based on type (similar to hotspots)\n if (portal.type === 'sphere') {\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(portal.color || '#9C27B0'); // Purple default for portals\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // For spheres: use full opacity with depth write for proper GSplat occlusion\n const sphereOpacity = portal.opacity ?? 0.8;\n if (sphereOpacity >= 0.95) {\n material.opacity = 1;\n material.blendType = pc.BLEND_NONE;\n material.depthTest = true;\n material.depthWrite = true;\n } else {\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true;\n }\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n } else if (portal.type === 'image' && portal.imageUrl) {\n entity.addComponent('render', {\n type: 'plane',\n castShadows: false,\n receiveShadows: false\n });\n\n const useLighting = portal.useLighting === true;\n const initialOpacity = portal.opacity ?? 1;\n\n (entity as any).textureLoaded = false;\n (entity as any).hiddenUntilTextureLoaded = true;\n entity.enabled = false;\n\n const material = createImageMaterial(portal.imageUrl, useLighting, initialOpacity, () => {\n (entity as any).textureLoaded = true;\n if (!(entity as any).visibilityRange || (entity as any).shouldBeVisible) {\n entity.enabled = true;\n }\n (entity as any).hiddenUntilTextureLoaded = false;\n });\n entity.render!.material = material;\n entity.setLocalScale(sx, sy, sz);\n entity.rotateLocal(90, 180, 0);\n\n (entity as any).portalMaterial = material;\n console.log(`[Portal] Created image portal: ${portal.title || portal.targetSceneName || 'Untitled'}`);\n } else {\n // Default: sphere\n entity.addComponent('render', {\n type: 'sphere',\n castShadows: false,\n receiveShadows: false\n });\n\n const material = new pc.StandardMaterial();\n const color = parseColor(portal.color || '#9C27B0');\n material.diffuse = color;\n material.emissive = color.clone();\n (material.emissive as pc.Color).mulScalar(0.5);\n\n // Use additive blending for better GSplat compatibility\n const sphereOpacity = portal.opacity ?? 0.8;\n material.opacity = sphereOpacity;\n material.blendType = pc.BLEND_ADDITIVEALPHA;\n material.depthTest = true;\n material.depthWrite = true;\n material.update();\n\n entity.render!.material = material;\n entity.setLocalScale(sx * 0.2, sy * 0.2, sz * 0.2);\n }\n\n // Add collision for raycasting\n entity.addComponent('collision', {\n type: portal.type === 'sphere' ? 'sphere' : 'box',\n radius: 0.1,\n halfExtents: new pc.Vec3(0.5, 0.5, 0.05)\n });\n\n // Store portal data for click handling\n (entity as any).portalData = portal;\n\n // Billboard mode\n if (portal.billboard) {\n app.on('update', () => {\n if (entity.enabled) {\n entity.lookAt(camera.getPosition());\n entity.rotateLocal(90, 180, 0);\n }\n });\n }\n\n // Visibility range\n if (portal.visibilityRange) {\n (entity as any).visibilityRange = portal.visibilityRange;\n entity.enabled = false;\n }\n\n app.root.addChild(entity);\n portalEntities.push(entity);\n\n console.log(`[StorySplat Viewer] Created portal: ${portal.title || portal.targetSceneName || 'Untitled'} -> ${portal.targetSceneId}`);\n });\n }\n\n // Update portal visibility based on progress\n function updatePortalVisibility(): void {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n const cameraPos = camera.getPosition();\n\n portalEntities.forEach((entity: any) => {\n const portal = entity.portalData;\n if (!portal) return;\n\n // Determine if portal should be visible based on visibility range\n let shouldBeVisible = true;\n const range = entity.visibilityRange;\n if (range) {\n if (range.type === 'waypoint') {\n shouldBeVisible = waypointIndex >= range.start && waypointIndex <= range.end;\n } else if (range.type === 'percentage') {\n shouldBeVisible = scrollPercent >= range.start && scrollPercent <= range.end;\n }\n }\n\n entity.shouldBeVisible = shouldBeVisible;\n\n // Enable/disable based on visibility and texture loaded state\n if (entity.hiddenUntilTextureLoaded) {\n entity.enabled = false;\n } else {\n entity.enabled = shouldBeVisible;\n }\n\n // Proximity activation check\n if (portal.activationMode === 'proximity' && shouldBeVisible) {\n const portalPos = entity.getPosition();\n const distance = cameraPos.distance(portalPos);\n const proximityDistance = portal.proximityDistance || 2;\n\n if (distance <= proximityDistance && !entity.proximityTriggered) {\n entity.proximityTriggered = true;\n console.log(`[Portal] Proximity triggered: ${portal.title || portal.targetSceneName}, navigating to scene ${portal.targetSceneId}`);\n handlePortalNavigation(portal);\n } else if (distance > proximityDistance) {\n entity.proximityTriggered = false;\n }\n }\n });\n }\n\n // Listen for progress updates to update portal visibility\n events.on('progressUpdate', () => {\n updatePortalVisibility();\n });\n\n // Also call initially after portals are created\n setTimeout(() => {\n updatePortalVisibility();\n }, 100);\n\n // Raycast helper for portal detection\n function raycastPortals(x: number, y: number): { entity: any; portal: any } | null {\n const from = camera.camera!.screenToWorld(x, y, camera.camera!.nearClip);\n const to = camera.camera!.screenToWorld(x, y, camera.camera!.farClip);\n\n let closestHit: { entity: any; distance: number } | null = null;\n\n portalEntities.forEach(entity => {\n if (!entity.enabled) return;\n\n const pos = entity.getPosition();\n const dir = new pc.Vec3().sub2(to, from).normalize();\n const toEntity = new pc.Vec3().sub2(pos, from);\n\n const t = toEntity.dot(dir);\n if (t < 0) return;\n\n const closestPoint = new pc.Vec3().add2(from, dir.clone().mulScalar(t));\n const distance = closestPoint.distance(pos);\n\n const entityScale = entity.getLocalScale();\n const hitRadius = Math.max(entityScale.x, entityScale.y, 0.3) * 0.6;\n\n if (distance < hitRadius) {\n if (!closestHit || t < closestHit.distance) {\n closestHit = { entity, distance: t };\n }\n }\n });\n\n if (closestHit !== null) {\n const entity = (closestHit as any).entity;\n return { entity, portal: entity.portalData };\n }\n return null;\n }\n\n // Handle portal navigation (scene-to-scene)\n async function handlePortalNavigation(portal: any): Promise<void> {\n if (!portal.targetSceneId) {\n console.warn('[Portal] No target scene ID specified');\n return;\n }\n\n console.log(`[StorySplat Viewer] Portal activated: navigating to scene ${portal.targetSceneId}`);\n\n // Emit event for external handlers\n events.emit('portalActivated', {\n portalId: portal.id,\n targetSceneId: portal.targetSceneId,\n targetSceneName: portal.targetSceneName\n });\n\n // Show loading UI\n showPortalLoadingUI(container, portal.targetSceneName || portal.targetSceneId);\n\n try {\n // Fetch the new scene data\n const sceneApiUrl = `https://discover.storysplat.com/api/scene/${portal.targetSceneId}`;\n console.log(`[Portal] Fetching scene from: ${sceneApiUrl}`);\n\n const response = await fetch(sceneApiUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch scene: ${response.status} ${response.statusText}`);\n }\n\n const result = await response.json();\n const newSceneData = result.data || result;\n\n // Store current instance reference before cleanup\n const currentInstance = instance;\n\n // Cleanup current scene (iOS memory management)\n cleanupForPortalNavigation();\n\n // Small delay for garbage collection (iOS)\n await new Promise(resolve => setTimeout(resolve, 100));\n\n // Create new viewer with the fetched scene data\n // Note: We're recreating in the same container, so we need to remove the current canvas first\n if (canvas && canvas.parentNode) {\n canvas.remove();\n }\n\n // Create new viewer (this will replace 'instance' reference for external code)\n const newViewer = await createViewer(container, newSceneData, {});\n\n // Hide loading UI\n hidePortalLoadingUI(container);\n\n console.log(`[Portal] Successfully navigated to scene: ${portal.targetSceneId}`);\n\n // Return the new viewer instance (for programmatic use)\n return newViewer as any;\n } catch (error) {\n console.error('[Portal] Navigation failed:', error);\n hidePortalLoadingUI(container);\n\n // Show error message\n const errorDiv = document.createElement('div');\n errorDiv.className = 'storysplat-portal-error';\n errorDiv.textContent = `Failed to load scene: ${(error as Error).message}`;\n Object.assign(errorDiv.style, {\n position: 'fixed',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n background: 'rgba(0,0,0,0.9)',\n color: '#ff6b6b',\n padding: '20px',\n borderRadius: '8px',\n zIndex: '10000',\n fontFamily: 'system-ui, sans-serif'\n });\n container.appendChild(errorDiv);\n setTimeout(() => errorDiv.remove(), 3000);\n }\n }\n\n // Cleanup for portal navigation (memory management)\n function cleanupForPortalNavigation(): void {\n console.log('[Portal] Cleaning up current scene for navigation...');\n\n // Stop playback\n pause();\n\n // Stop all hotspot videos\n hotspotEntities.forEach((entity: any) => {\n if (entity.videoElement) {\n entity.videoElement.pause();\n entity.videoElement.src = '';\n }\n if (entity.alphaVideoElement) {\n entity.alphaVideoElement.pause();\n entity.alphaVideoElement.src = '';\n }\n });\n\n // Cleanup animated GIFs\n animatedGifs.forEach(gif => gif.destroy());\n animatedGifs.length = 0;\n\n // Cleanup custom meshes\n cleanupCustomMeshes();\n\n // Destroy hotspot entities\n hotspotEntities.forEach(entity => {\n entity.destroy();\n });\n hotspotEntities.length = 0;\n\n // Destroy portal entities\n portalEntities.forEach(entity => {\n entity.destroy();\n });\n portalEntities.length = 0;\n\n // Destroy splat entity\n if (splatEntity) {\n splatEntity.destroy();\n splatEntity = null;\n }\n\n // Cleanup HTML Mesh Manager\n if ((app as any).__htmlMeshManager) {\n (app as any).__htmlMeshManager.destroy();\n }\n\n // Cleanup Custom Script System\n if ((app as any).__customScriptSystem) {\n (app as any).__customScriptSystem.dispose();\n }\n\n console.log('[Portal] Cleanup complete');\n }\n\n // Show portal loading UI\n function showPortalLoadingUI(container: HTMLElement, sceneName: string): void {\n // Remove any existing loading UI\n const existing = container.querySelector('.storysplat-portal-loading');\n if (existing) existing.remove();\n\n const loadingDiv = document.createElement('div');\n loadingDiv.className = 'storysplat-portal-loading';\n\n const spinner = document.createElement('div');\n spinner.className = 'storysplat-portal-spinner';\n\n const text = document.createElement('div');\n text.className = 'storysplat-portal-loading-text';\n text.textContent = `Loading ${sceneName}...`;\n\n loadingDiv.appendChild(spinner);\n loadingDiv.appendChild(text);\n\n // Styles\n Object.assign(loadingDiv.style, {\n position: 'fixed',\n top: '0',\n left: '0',\n width: '100%',\n height: '100%',\n background: 'rgba(0, 0, 0, 0.85)',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: '10000',\n fontFamily: 'system-ui, sans-serif'\n });\n\n Object.assign(spinner.style, {\n width: '50px',\n height: '50px',\n border: '4px solid rgba(255, 255, 255, 0.2)',\n borderTop: '4px solid #9C27B0',\n borderRadius: '50%',\n animation: 'storysplat-portal-spin 1s linear infinite',\n marginBottom: '20px'\n });\n\n Object.assign(text.style, {\n color: '#ffffff',\n fontSize: '18px'\n });\n\n // Add keyframes for spinner animation\n if (!document.getElementById('storysplat-portal-styles')) {\n const styleEl = document.createElement('style');\n styleEl.id = 'storysplat-portal-styles';\n styleEl.textContent = `\n @keyframes storysplat-portal-spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n }\n `;\n document.head.appendChild(styleEl);\n }\n\n container.appendChild(loadingDiv);\n }\n\n // Hide portal loading UI\n function hidePortalLoadingUI(container: HTMLElement): void {\n const loadingDiv = container.querySelector('.storysplat-portal-loading');\n if (loadingDiv) {\n loadingDiv.remove();\n }\n }\n\n // Raycast helper for hotspot detection\n const canvasEl = app.graphicsDevice.canvas as HTMLCanvasElement;\n\n function raycastHotspots(x: number, y: number): { entity: any; hotspot: any } | null {\n const from = camera.camera!.screenToWorld(x, y, camera.camera!.nearClip);\n const to = camera.camera!.screenToWorld(x, y, camera.camera!.farClip);\n\n let closestHit: { entity: any; distance: number } | null = null;\n\n hotspotEntities.forEach(entity => {\n if (!entity.enabled) return;\n\n const pos = entity.getPosition();\n const dir = new pc.Vec3().sub2(to, from).normalize();\n const toEntity = new pc.Vec3().sub2(pos, from);\n\n const t = toEntity.dot(dir);\n if (t < 0) return;\n\n const closestPoint = new pc.Vec3().add2(from, dir.clone().mulScalar(t));\n const distance = closestPoint.distance(pos);\n\n // Use the entity's actual scale to determine hit radius\n // Take the maximum of X and Y scale for a reasonable hit area\n const entityScale = entity.getLocalScale();\n const hitRadius = Math.max(entityScale.x, entityScale.y, 0.3) * 0.6; // Min 0.18 units, scaled by 0.6\n\n if (distance < hitRadius) {\n if (!closestHit || t < closestHit.distance) {\n closestHit = { entity, distance: t };\n }\n }\n });\n\n if (closestHit !== null) {\n const entity = (closestHit as any).entity;\n return { entity, hotspot: entity.hotspotData };\n }\n return null;\n }\n\n // Hover detection for cursor change and hover popups\n let currentHoverHotspot: any = null;\n let isMouseOverPopup = false;\n\n // Track when mouse is over the popup\n const popup = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n const overlay = container.querySelector('.storysplat-hotspot-overlay') as HTMLElement;\n\n if (popup) {\n popup.addEventListener('mouseenter', () => {\n isMouseOverPopup = true;\n });\n popup.addEventListener('mouseleave', () => {\n isMouseOverPopup = false;\n // Close popup when mouse leaves the popup (for hover-activated hotspots)\n if (currentHoverHotspot && currentHoverHotspot.activationMode === 'hover') {\n popup.classList.remove('visible');\n if (overlay) overlay.classList.remove('visible');\n currentHoverHotspot = null;\n }\n });\n }\n\n canvasEl.addEventListener('mousemove', (e: MouseEvent) => {\n const rect = canvasEl.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Check portals first\n const portalHit = raycastPortals(x, y);\n if (portalHit && portalHit.portal) {\n const portal = portalHit.portal;\n // Show pointer cursor for click-activated portals\n const effectiveActivationMode = portal.activationMode || 'click';\n if (effectiveActivationMode === 'click') {\n canvasEl.style.cursor = 'pointer';\n } else {\n canvasEl.style.cursor = 'default';\n }\n return; // Portals take precedence over hotspots\n }\n\n const hit = raycastHotspots(x, y);\n if (hit && hit.hotspot) {\n const hotspot = hit.hotspot;\n\n // Show pointer cursor for interactive hotspots\n if (hotspot.activationMode === 'click' || hotspot.activationMode === 'hover' || hotspot.type === 'video') {\n canvasEl.style.cursor = 'pointer';\n } else {\n canvasEl.style.cursor = 'default';\n }\n\n // Show popup on hover for hover-activated hotspots\n if (hotspot.activationMode === 'hover' && currentHoverHotspot !== hotspot) {\n currentHoverHotspot = hotspot;\n if (hotspot.information || hotspot.photoUrl || hotspot.iframeUrl || hotspot.externalLinkUrl) {\n showHotspotPopup(container, hotspot);\n }\n }\n } else {\n canvasEl.style.cursor = 'default';\n\n // Only hide popup if mouse is NOT over the popup element\n if (currentHoverHotspot && currentHoverHotspot.activationMode === 'hover' && !isMouseOverPopup) {\n const popupEl = container.querySelector('.storysplat-hotspot-popup') as HTMLElement;\n const overlayEl = container.querySelector('.storysplat-hotspot-overlay') as HTMLElement;\n if (popupEl) popupEl.classList.remove('visible');\n if (overlayEl) overlayEl.classList.remove('visible');\n currentHoverHotspot = null;\n }\n }\n });\n\n // Click handling for hotspots and portals\n canvasEl.addEventListener('click', (e: MouseEvent) => {\n const rect = canvasEl.getBoundingClientRect();\n const x = e.clientX - rect.left;\n const y = e.clientY - rect.top;\n\n // Check for portal clicks first (portals take precedence)\n const portalHit = raycastPortals(x, y);\n if (portalHit !== null && portalHit.portal) {\n const portal = portalHit.portal;\n console.log('[StorySplat Viewer] Portal clicked:', portal.title || portal.targetSceneName);\n\n // Handle click-activated portals\n const effectiveActivationMode = portal.activationMode || 'click';\n if (effectiveActivationMode === 'click') {\n handlePortalNavigation(portal);\n }\n return; // Don't process hotspot clicks when portal is clicked\n }\n\n const hitResult = raycastHotspots(x, y);\n if (hitResult !== null) {\n const entity = hitResult.entity;\n const hotspot = hitResult.hotspot;\n\n console.log('[StorySplat Viewer] Hotspot clicked:', hotspot.title);\n\n // Handle hotspot audio play/pause (spatial or non-spatial)\n if (entity.audioElements && entity.audioElements.audio) {\n const audioElements = entity.audioElements as HotspotAudioElements;\n const audio = audioElements.audio;\n\n if (audio.paused) {\n // Resume AudioContext on user interaction (browser requirement)\n if (audioElements.audioCtx && audioElements.audioCtx.state === 'suspended') {\n audioElements.audioCtx.resume();\n }\n audio.play().catch(err => console.error('[Audio] Hotspot audio play failed:', err));\n console.log('[Audio] Hotspot audio started:', hotspot.title);\n } else {\n audio.pause();\n console.log('[Audio] Hotspot audio paused:', hotspot.title);\n }\n }\n\n // Handle video toggle (for click or autoplay mode - autoplay needs click to unmute)\n if (entity.videoElement && (entity.mediaTriggerMode === 'click' || entity.mediaTriggerMode === 'autoplay')) {\n const video = entity.videoElement as HTMLVideoElement;\n const alphaVideo = entity.alphaVideoElement as HTMLVideoElement | null;\n\n if (entity.mediaTriggerMode === 'autoplay' && !video.paused && video.muted) {\n // Autoplay mode: click unmutes if already playing\n video.muted = hotspot.videoMuted === false ? false : false; // Unmute on click\n console.log('[Hotspot] Video unmuted by click');\n } else if (video.paused) {\n // Play video\n playVideoHotspot(entity, hotspot);\n console.log('[Hotspot] Video started');\n } else {\n // Pause video\n pauseVideoHotspot(entity);\n console.log('[Hotspot] Video paused');\n }\n }\n\n // Show popup for click-activated hotspots with content\n // Default to 'click' if activationMode is not set\n const effectiveActivationMode = hotspot.activationMode || 'click';\n // Show popup if there's any displayable content (title, information, photo, iframe, or link)\n const hasPopupContent = hotspot.title || hotspot.information || hotspot.photoUrl || hotspot.iframeUrl || hotspot.externalLinkUrl;\n if (effectiveActivationMode === 'click' && hasPopupContent) {\n showHotspotPopup(container, hotspot);\n }\n\n // Handle hotspot teleportation (teleport to waypoint or percentage)\n const teleportWaypoint = hotspot.teleportWaypoint ?? (hotspot as any).teleportToWaypoint;\n const teleportPercent = hotspot.teleportPercent ?? (hotspot as any).teleportToPercent;\n const teleportMode = hotspot.teleportMode || 'animate'; // Default to animate for smooth experience\n\n let calculatedTargetProgress: number | null = null;\n\n if (teleportWaypoint !== undefined && teleportWaypoint !== -1) {\n // Teleport to specific waypoint\n const numWaypoints = config.waypoints?.length || 1;\n const targetIndex = Math.max(0, Math.min(teleportWaypoint, numWaypoints - 1));\n calculatedTargetProgress = numWaypoints > 1 ? targetIndex / (numWaypoints - 1) : 0;\n console.log('[Hotspot] Teleporting to waypoint:', targetIndex, '(progress:', calculatedTargetProgress, ', mode:', teleportMode, ')');\n } else if (teleportPercent !== undefined && teleportPercent !== -1) {\n // Teleport to specific percentage (0-100)\n calculatedTargetProgress = Math.max(0, Math.min(teleportPercent / 100, 1));\n console.log('[Hotspot] Teleporting to percent:', teleportPercent, '(progress:', calculatedTargetProgress, ', mode:', teleportMode, ')');\n }\n\n if (calculatedTargetProgress !== null) {\n if (teleportMode === 'instant') {\n // Instant jump - directly set position\n currentProgress = calculatedTargetProgress;\n updateCameraFromProgress(currentProgress);\n } else {\n // Animate (default) - smooth transition\n animateToProgress(calculatedTargetProgress, 800); // 800ms for smooth transition\n }\n }\n }\n });\n\n // Double-click/double-tap to focus camera on clicked point (explore mode only)\n async function handleDoubleClickFocus(screenX: number, screenY: number): Promise<void> {\n if (currentCameraMode !== 'explore') return;\n\n console.log('[StorySplat Viewer] Double-click focus at:', screenX, screenY);\n\n // First check if we hit a hotspot\n const hotspotHit = raycastHotspots(screenX, screenY);\n if (hotspotHit) {\n const pos = hotspotHit.entity.getPosition();\n console.log('[StorySplat Viewer] Focusing on hotspot at:', pos.x, pos.y, pos.z);\n cameraControls.focus(pos, false);\n return;\n }\n\n // Try to pick the GSplat using pc.Picker with depth buffer (PlayCanvas 2.14+)\n try {\n // Use 25% scale for performance (matching PlayCanvas official picking example)\n const pickerScale = 0.25;\n picker.resize(\n Math.floor(canvasEl.clientWidth * pickerScale),\n Math.floor(canvasEl.clientHeight * pickerScale)\n );\n\n const worldLayer = app.scene.layers.getLayerByName('World');\n if (!worldLayer) {\n console.warn('[StorySplat Viewer] World layer not found');\n return;\n }\n\n picker.prepare(camera.camera!, app.scene, [worldLayer]);\n\n // Calculate scaled pick coordinates\n const scaledX = Math.floor(screenX * pickerScale);\n const scaledY = Math.floor(screenY * pickerScale);\n\n // Use getWorldPointAsync to get the actual 3D intersection point directly\n // This is the proper way to pick splats with depth-enabled picker\n const worldPoint = await picker.getWorldPointAsync(scaledX, scaledY);\n\n if (worldPoint) {\n // Validate the point is reasonable (not at infinity or behind camera)\n const cameraPos = camera.getPosition();\n const distance = cameraPos.distance(worldPoint);\n\n if (distance > 0.1 && distance < 1000) {\n console.log('[StorySplat Viewer] Focusing on GSplat at:',\n worldPoint.x.toFixed(2), worldPoint.y.toFixed(2), worldPoint.z.toFixed(2),\n 'distance:', distance.toFixed(2));\n cameraControls.focus(worldPoint, false);\n return;\n }\n }\n\n // Fallback: try mesh instance selection if world point failed\n const meshInstances = await picker.getSelectionAsync(scaledX, scaledY, 1, 1);\n\n if (meshInstances.length > 0) {\n const mi = meshInstances[0];\n // Use AABB center as a rough focus point\n const aabbCenter = mi.aabb.center.clone();\n console.log('[StorySplat Viewer] Focusing on mesh AABB center:',\n aabbCenter.x.toFixed(2), aabbCenter.y.toFixed(2), aabbCenter.z.toFixed(2));\n cameraControls.focus(aabbCenter, false);\n } else {\n console.log('[StorySplat Viewer] No pick result at click point');\n }\n } catch (err) {\n console.warn('[StorySplat Viewer] Picking failed:', err);\n }\n }\n\n // Desktop double-click handler\n canvasEl.addEventListener('dblclick', (e: MouseEvent) => {\n const rect = canvasEl.getBoundingClientRect();\n handleDoubleClickFocus(e.clientX - rect.left, e.clientY - rect.top);\n });\n\n // Mobile double-tap detection\n let lastTapTime = 0;\n const DOUBLE_TAP_THRESHOLD = 300;\n\n canvasEl.addEventListener('touchend', (e: TouchEvent) => {\n if (e.changedTouches.length !== 1) return;\n const now = Date.now();\n if (now - lastTapTime < DOUBLE_TAP_THRESHOLD) {\n const touch = e.changedTouches[0];\n const rect = canvasEl.getBoundingClientRect();\n handleDoubleClickFocus(touch.clientX - rect.left, touch.clientY - rect.top);\n lastTapTime = 0;\n } else {\n lastTapTime = now;\n }\n });\n\n // Load the splat after app starts\n updateProgress(0.2, 'Initializing...');\n\n loadSplat().then(() => {\n updateProgress(1.0, 'Ready!');\n\n // Create hotspots after splat is loaded\n createHotspots();\n\n // Create portals for scene-to-scene navigation\n createPortals();\n\n // Setup waypoint audio (audio interactions on waypoints)\n setupWaypointAudio();\n\n // Initialize particle systems\n console.log('[StorySplat Viewer] 🎆 Calling initParticleSystems...');\n initParticleSystems();\n\n // Initialize custom meshes (3D models with animations, audio)\n initCustomMeshes();\n\n // Initialize skybox\n initSkybox();\n\n // Initialize custom lights\n initCustomLights();\n\n // Start reveal effect IMMEDIATELY (before preloader hides)\n // The preloader is semi-transparent (0.85 opacity) so users can see the reveal through it\n if (revealScript) {\n revealScript.enabled = true;\n console.log('[StorySplat Viewer] Reveal effect started immediately');\n }\n\n // Hide preloader AFTER a short delay to let reveal effect start\n // This ensures users see the beginning of the reveal animation through the semi-transparent preloader\n setTimeout(() => {\n if (uiElements.preloader) {\n hidePreloader(uiElements.preloader);\n }\n }, 200);\n\n // Setup XR (VR/AR) if enabled\n setupXR();\n\n // Setup HTML Meshes if configured\n if (config.htmlMeshes && config.htmlMeshes.length > 0) {\n console.log('[StorySplat Viewer] Setting up HTML meshes:', config.htmlMeshes.length);\n const htmlMeshManager = setupHtmlMeshes(app, config.htmlMeshes);\n // Store manager reference for cleanup\n (app as any).__htmlMeshManager = htmlMeshManager;\n\n // Listen for progress updates to update HTML mesh visibility\n events.on('progressUpdate', () => {\n const scrollPercent = currentProgress * 100;\n const numWaypoints = config.waypoints?.length || 1;\n const waypointIndex = Math.round(currentProgress * Math.max(1, numWaypoints - 1));\n htmlMeshManager.updateVisibility(scrollPercent, waypointIndex);\n });\n }\n\n // Setup Custom Script System\n if (config.customScript && config.customScript.trim() !== '') {\n console.log('[StorySplat Viewer] Initializing custom script system...');\n const customScriptSystem = setupCustomScript(\n app,\n camera,\n canvas,\n config.customScript,\n () => currentProgress,\n () => currentWaypointIndex,\n () => hotspotEntities,\n () => splatEntity ? [splatEntity] : [],\n () => config.htmlMeshes || []\n );\n if (customScriptSystem) {\n // Store for cleanup\n (app as any).__customScriptSystem = customScriptSystem;\n console.log('[StorySplat Viewer] Custom script system initialized');\n }\n }\n\n // Check for URL parameters to support deep-linking\n // ?waypoint=2 will navigate to waypoint index 2\n // ?autoplay=true will start autoplay\n try {\n const urlParams = new URLSearchParams(window.location.search);\n const waypointParam = urlParams.get('waypoint');\n const autoplayParam = urlParams.get('autoplay');\n\n if (waypointParam !== null && config.waypoints && config.waypoints.length > 0) {\n const targetIndex = parseInt(waypointParam, 10);\n if (!isNaN(targetIndex) && targetIndex >= 0 && targetIndex < config.waypoints.length) {\n console.log('[StorySplat Viewer] URL param: navigating to waypoint', targetIndex);\n goToWaypoint(targetIndex);\n }\n }\n\n if (autoplayParam === 'true' && !options.autoPlay && !config.autoPlay) {\n console.log('[StorySplat Viewer] URL param: starting autoplay');\n play();\n }\n } catch (e) {\n // URLSearchParams may not be available in some environments\n console.warn('[StorySplat Viewer] Could not parse URL parameters:', e);\n }\n\n events.emit('ready');\n console.log('[StorySplat Viewer] Ready');\n\n // Connect UI to viewer controls\n if (showUI) {\n connectUIToViewer(uiElements, {\n nextWaypoint,\n prevWaypoint,\n play,\n pause,\n isPlaying: () => isPlaying,\n getCurrentWaypointIndex: () => currentWaypointIndex,\n getWaypointCount: () => config.waypoints?.length || 0,\n getWaypoints: () => config.waypoints || [],\n setCameraMode,\n on: (event, callback) => events.on(event, callback)\n }, defaultMode, uiOpts.buttonLabels);\n\n // Setup return to waypoint button click handler\n if (uiElements.returnWaypointButton && config.waypoints && config.waypoints.length > 0) {\n uiElements.returnWaypointButton.addEventListener('click', () => {\n // Find the nearest waypoint based on current camera position\n const cameraPos = camera.getPosition();\n let nearestIndex = 0;\n let nearestDistance = Infinity;\n\n config.waypoints!.forEach((wp, index) => {\n const wpPos = wp.position || [0, 0, 0];\n // Calculate distance (negate Z for coordinate conversion)\n const dx = cameraPos.x - wpPos[0];\n const dy = cameraPos.y - wpPos[1];\n const dz = cameraPos.z - (-wpPos[2]); // Negate Z for Babylon->PlayCanvas\n const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);\n\n if (distance < nearestDistance) {\n nearestDistance = distance;\n nearestIndex = index;\n }\n });\n\n console.log('[StorySplat Viewer] Returning to nearest waypoint:', nearestIndex, 'distance:', nearestDistance.toFixed(2));\n\n // Switch to tour mode\n setCameraMode('tour');\n\n // Navigate to the nearest waypoint\n goToWaypoint(nearestIndex);\n\n // Update mode toggle UI buttons\n const modeButtons = uiElements.scrollControls?.querySelectorAll('.storysplat-mode-btn');\n modeButtons?.forEach(btn => {\n const btnMode = btn.getAttribute('data-mode');\n btn.classList.toggle('selected', btnMode === 'tour');\n });\n });\n }\n\n // Emit initial progress to update UI\n events.emit('progressUpdate', { progress: currentProgress, index: currentWaypointIndex });\n\n // Emit initial waypoint change to show first waypoint info\n if (config.waypoints && config.waypoints.length > 0) {\n events.emit('waypointChange', {\n index: 0,\n waypoint: config.waypoints[0],\n prevIndex: -1\n });\n }\n }\n\n // Start autoplay if enabled via options OR via scene config\n if (options.autoPlay || config.autoPlay) {\n play();\n }\n }).catch(err => {\n console.error('[StorySplat Viewer] Failed to initialize:', err);\n // Hide preloader even on error\n if (uiElements.preloader) {\n hidePreloader(uiElements.preloader);\n }\n events.emit('error', err);\n });\n\n // Handle resize\n const handleResize = () => {\n app.resizeCanvas();\n };\n window.addEventListener('resize', handleResize);\n\n // Return viewer instance\n const instance: ViewerInstance = {\n app,\n canvas,\n\n // Navigation\n goToWaypoint,\n nextWaypoint,\n prevWaypoint,\n getCurrentWaypointIndex: () => currentWaypointIndex,\n getWaypointCount: () => config.waypoints?.length || 0,\n\n // Camera\n setPosition: (x, y, z) => camera.setPosition(x, y, z),\n setRotation: (x, y, z) => camera.setEulerAngles(x, y, z),\n getPosition: () => {\n const pos = camera.getPosition();\n return { x: pos.x, y: pos.y, z: pos.z };\n },\n getRotation: () => {\n const rot = camera.getEulerAngles();\n return { x: rot.x, y: rot.y, z: rot.z };\n },\n\n // Playback\n play,\n pause,\n stop,\n isPlaying: () => isPlaying,\n\n // Lifecycle\n destroy: () => {\n // Set destroyed flag first to prevent async operations from completing\n isDestroyed = true;\n pause();\n window.removeEventListener('resize', handleResize);\n // Remove UI elements\n if (uiElements.preloader) uiElements.preloader.remove();\n if (uiElements.scrollControls) uiElements.scrollControls.remove();\n if (uiElements.fullscreenButton) uiElements.fullscreenButton.remove();\n if (uiElements.helpButton) uiElements.helpButton.remove();\n if (uiElements.helpPanel) uiElements.helpPanel.remove();\n if (uiElements.waypointInfo) uiElements.waypointInfo.remove();\n if (uiElements.watermark) uiElements.watermark.remove();\n // Remove injected styles\n const styleEl = document.getElementById('storysplat-viewer-styles');\n if (styleEl) styleEl.remove();\n // Remove container class\n container.classList.remove('storysplat-viewer-container');\n // Cleanup animated GIFs\n animatedGifs.forEach(gif => gif.destroy());\n animatedGifs.length = 0;\n // Cleanup custom meshes\n cleanupCustomMeshes();\n // Cleanup CharacterController\n if (characterController) {\n characterController.destroy();\n }\n // Cleanup Custom Script System\n if ((app as any).__customScriptSystem) {\n (app as any).__customScriptSystem.dispose();\n }\n // Cleanup HTML Mesh Manager\n if ((app as any).__htmlMeshManager) {\n (app as any).__htmlMeshManager.destroy();\n }\n app.destroy();\n canvas.remove();\n },\n resize: handleResize,\n\n // Portal Navigation\n navigateToScene: async (sceneId: string) => {\n const portalData = { targetSceneId: sceneId, activationMode: 'click' };\n await handlePortalNavigation(portalData);\n },\n\n // Events\n on: (event: ViewerEvent, callback) => events.on(event, callback),\n off: (event: ViewerEvent, callback) => events.off(event, callback)\n };\n\n return instance;\n}\n\n/**\n * Create viewer from scene URL\n */\nexport async function createViewerFromUrl(\n container: HTMLElement,\n jsonUrl: string,\n options?: ViewerOptions\n): Promise<ViewerInstance> {\n console.log('[StorySplat Viewer] Fetching scene from:', jsonUrl);\n const response = await fetch(jsonUrl);\n if (!response.ok) {\n throw new Error(`Failed to fetch scene: ${response.statusText}`);\n }\n const scene: SceneData = await response.json();\n console.log('[StorySplat Viewer] Scene data loaded:', scene);\n return createViewer(container, scene, options);\n}\n\n// Utility functions\nfunction lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\nfunction lerpAngle(a: number, b: number, t: number): number {\n let diff = b - a;\n while (diff > 180) diff -= 360;\n while (diff < -180) diff += 360;\n return a + diff * t;\n}\n\nfunction easeInOutCubic(t: number): number {\n return t < 0.5\n ? 4 * t * t * t\n : 1 - Math.pow(-2 * t + 2, 3) / 2;\n}\n","/**\n * Create viewer from StorySplat Scene ID\n *\n * This module provides functionality to create a viewer by fetching\n * scene data from the StorySplat API using a scene ID.\n */\n\nimport { createViewer } from './createViewer';\nimport type { ViewerInstance, ViewerOptions, SceneData } from '../types';\n\n/**\n * Options for createViewerFromSceneId\n */\nexport interface ViewerFromSceneIdOptions extends ViewerOptions {\n /**\n * Base URL for the StorySplat API\n * @default 'https://discover.storysplat.com'\n */\n baseUrl?: string;\n\n /**\n * API key for accessing private scenes (future feature)\n */\n apiKey?: string;\n}\n\n/**\n * API response format from /api/scene/{sceneId}\n */\ninterface SceneApiResponse {\n success: boolean;\n data: SceneData;\n meta: {\n sceneId: string;\n name: string;\n description?: string;\n userName?: string; // May be undefined for anonymous/deleted users\n userSlug?: string; // May be undefined for anonymous/deleted users\n thumbnailUrl: string;\n splatUrl: string;\n htmlUrl: string;\n playcanvasHtmlUrl?: string;\n views: number;\n createdAt: string | null;\n };\n}\n\n/**\n * Error thrown when scene fetching fails\n */\nexport class SceneNotFoundError extends Error {\n constructor(sceneId: string) {\n super(`Scene not found: ${sceneId}`);\n this.name = 'SceneNotFoundError';\n }\n}\n\n/**\n * Error thrown when API request fails\n */\nexport class SceneApiError extends Error {\n public statusCode: number;\n\n constructor(message: string, statusCode: number) {\n super(message);\n this.name = 'SceneApiError';\n this.statusCode = statusCode;\n }\n}\n\n/**\n * Get the default API base URL from environment or fallback to production\n */\nfunction getDefaultBaseUrl(): string {\n // Check for environment variable (works in Node.js, bundlers with define, etc.)\n if (typeof process !== 'undefined' && process.env?.STORYSPLAT_API_URL) {\n return process.env.STORYSPLAT_API_URL;\n }\n // Check for window-based config (for browser environments)\n if (typeof window !== 'undefined' && (window as any).__STORYSPLAT_API_URL__) {\n return (window as any).__STORYSPLAT_API_URL__;\n }\n // Default to production\n return 'https://discover.storysplat.com';\n}\n\n/**\n * Default API base URL\n */\nconst DEFAULT_BASE_URL = getDefaultBaseUrl();\n\n/**\n * Create a viewer from a StorySplat scene ID\n *\n * This function fetches scene data from the StorySplat API and creates\n * an embedded viewer. The scene must be public and owned by a user\n * with a public profile.\n *\n * @param container - HTML element to render the viewer in\n * @param sceneId - StorySplat scene ID from your dashboard\n * @param options - Viewer options including API configuration\n * @returns Promise resolving to ViewerInstance\n *\n * @example\n * ```typescript\n * import { createViewerFromSceneId } from 'storysplat-viewer';\n *\n * const viewer = await createViewerFromSceneId(\n * document.getElementById('viewer')!,\n * 'YOUR_SCENE_ID'\n * );\n *\n * // Control playback\n * viewer.play();\n * viewer.pause();\n *\n * // Navigate\n * viewer.nextWaypoint();\n * viewer.goToWaypoint(2);\n *\n * // Listen for events\n * viewer.on('ready', () => console.log('Viewer ready!'));\n * ```\n */\nexport async function createViewerFromSceneId(\n container: HTMLElement,\n sceneId: string,\n options: ViewerFromSceneIdOptions = {}\n): Promise<ViewerInstance> {\n const baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n\n console.log(`[StorySplat Viewer] Fetching scene: ${sceneId}`);\n\n // Build request URL\n const apiUrl = `${baseUrl}/api/scene/${encodeURIComponent(sceneId)}`;\n\n // Build headers\n const headers: HeadersInit = {\n 'Content-Type': 'application/json',\n };\n\n // Add API key if provided (for future private scene support)\n if (options.apiKey) {\n headers['Authorization'] = `Bearer ${options.apiKey}`;\n }\n\n // Fetch scene data\n const response = await fetch(apiUrl, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new SceneNotFoundError(sceneId);\n }\n\n const errorText = await response.text();\n let errorMessage: string;\n\n try {\n const errorJson = JSON.parse(errorText);\n errorMessage = errorJson.error || `API error: ${response.status}`;\n } catch {\n errorMessage = `API error: ${response.status}`;\n }\n\n throw new SceneApiError(errorMessage, response.status);\n }\n\n const apiResponse: SceneApiResponse = await response.json();\n\n if (!apiResponse.success || !apiResponse.data) {\n throw new SceneApiError('Invalid API response format', 500);\n }\n\n console.log(`[StorySplat Viewer] Scene loaded: \"${apiResponse.meta.name}\"`);\n\n // Merge scene metadata into scene data if not present\n const sceneData: SceneData = {\n ...apiResponse.data,\n // Ensure these are set from metadata if not in scene data\n name: apiResponse.data.name || apiResponse.meta.name,\n thumbnailUrl: apiResponse.data.thumbnailUrl || apiResponse.meta.thumbnailUrl,\n };\n\n // Extract viewer-specific options (remove API-specific ones)\n const { baseUrl: _baseUrl, apiKey: _apiKey, ...viewerOptions } = options;\n\n // Create and return the viewer\n return createViewer(container, sceneData, viewerOptions);\n}\n\n/**\n * Fetch scene metadata without creating a viewer\n *\n * Useful for displaying scene information before loading the full viewer.\n *\n * @param sceneId - StorySplat scene ID\n * @param options - API configuration options\n * @returns Promise resolving to scene metadata\n *\n * @example\n * ```typescript\n * import { fetchSceneMeta } from 'storysplat-viewer';\n *\n * const meta = await fetchSceneMeta('YOUR_SCENE_ID');\n * console.log(`Scene: ${meta.name} by ${meta.userName}`);\n * console.log(`Views: ${meta.views}`);\n * ```\n */\nexport async function fetchSceneMeta(\n sceneId: string,\n options: { baseUrl?: string; apiKey?: string } = {}\n): Promise<{\n name: string;\n description: string;\n thumbnailUrl: string;\n userName: string; // Defaults to 'Unknown' if not available\n userSlug: string; // Defaults to 'unknown' if not available\n views: number;\n tags: string[];\n category?: string;\n createdAt: string | null;\n}> {\n const baseUrl = options.baseUrl || DEFAULT_BASE_URL;\n const apiUrl = `${baseUrl}/api/scene/${encodeURIComponent(sceneId)}/meta`;\n\n const headers: HeadersInit = {\n 'Content-Type': 'application/json',\n };\n\n if (options.apiKey) {\n headers['Authorization'] = `Bearer ${options.apiKey}`;\n }\n\n const response = await fetch(apiUrl, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n if (response.status === 404) {\n throw new SceneNotFoundError(sceneId);\n }\n throw new SceneApiError(`API error: ${response.status}`, response.status);\n }\n\n const data = await response.json();\n\n // Ensure userName and userSlug have fallback values\n return {\n ...data,\n userName: data.userName || 'Unknown',\n userSlug: data.userSlug || 'unknown',\n };\n}\n"],"names":["escapeHtml","str","replace","generateHTML","sceneData","options","cdnUrl","title","name","description","faviconUrl","customCSS","faviconTag","customCSSBlock","rawJSON","JSON","stringify","key","value","HTMLElement","sceneDataJSON","async","generateHTMLFromUrl","jsonUrl","response","fetch","ok","Error","statusText","json","transformSceneToExportProps","scene","originalUrl","loadedModelUrl","splatUrl","sogUrl","sogModelUrl","compressedPlyUrl","lodMetaUrl","originalExt","split","pop","toLowerCase","isPlayCanvasCompatible","includes","console","warn","log","original","selected","waypoints","map","wp","fov","Math","PI","info","interactions","infoInteraction","find","i","type","data","text","position","x","y","z","rotation","duration","triggerDistance","sceneId","userId","userName","thumbnailUrl","fallbackUrls","filter","url","scale","splatScale","splatPosition","cameraPosition","splatRotation","cameraRotation","invertXScale","invertYScale","hotspots","portals","skybox","customMeshes","htmlMeshes","lights","particles","collisionMeshesData","playerHeight","uiColor","uiOptions","showStartExperience","showWatermark","hideFullscreenButton","hideInfoButton","hideMuteButton","hideHelpButton","hideWatermark","watermarkText","watermarkLink","buttonPosition","buttonLabels","customPreloaderLogoUrl","defaultCameraMode","allowedCameraModes","cameraMovementSpeed","cameraRotationSensitivity","invertCameraRotation","includeScrollControls","scrollButtonMode","scrollAmount","scrollSpeed","transitionSpeed","autoPlayEnabled","autoplaySpeed","loopMode","includeXR","xrMode","customScript","templateType","additionalSplats","keepMeshesInMemory","exportPropsToViewerConfig","props","cameraMode","autoPlay","nearClip","minClipPlane","farClip","maxClipPlane","DEFAULT_BUTTON_LABELS","tour","explore","hybrid","walk","orbit","next","previous","startExperience","returnToTour","fullscreen","mute","unmute","helpTitle","percentageFormat","getButtonLabel","labels","injectStyles","existingStyle","document","getElementById","remove","style","createElement","id","textContent","generateViewerStyles","head","appendChild","createPreloader","container","customLogoUrl","preloader","className","useCustomLogo","innerHTML","Promise","resolve","customElements","get","existingScript","querySelector","addEventListener","script","src","onload","hidePreloader","classList","add","setTimeout","connectUIToViewer","elements","viewer","prevBtn","scrollControls","nextBtn","playBtn","prevWaypoint","nextWaypoint","updatePlayButtonIcon","isPlaying","pause","play","on","helpButton","helpPanel","toggle","fullscreenButton","test","navigator","userAgent","platform","maxTouchPoints","display","parentElement","doc","fullscreenElement","webkitFullscreenElement","exitFullscreen","webkitExitFullscreen","expandIcon","compressIcon","requestFullscreen","webkitRequestFullscreen","handleFullscreenChange","isFullscreen","setTourControlsVisible","visible","progressText","progressContainer","scrollButtons","setCameraMode","modeButtons","querySelectorAll","forEach","btn","mode","getAttribute","b","btnMode","lastProgressUpdate","lastDisplayedPercentage","progress","percentage","max","min","roundedPercentage","round","now","performance","progressBar","width","String","lastDisplayedWaypointIndex","index","waypoint","waypointInfo","titleEl","descEl","getWaypoints","showHotspotPopup","hotspot","popup","contentEl","closeBtn","cssText","activationMode","hasMediaContent","photoUrl","popupVideoUrl","contentType","iframeUrl","backgroundColor","textColor","color","fontFamily","fontSize","closeButtonColor","contentHtml","information","externalLinkUrl","btnColor","externalLinkButtonColor","externalLinkText","setJoystickVisible","joystick","lookZone","updateJoystickPosition","active","dx","dy","maxRadius","joystickThumb","distance","sqrt","clampedDistance","clampedX","clampedY","transform","setCameraModeToggleVisible","cameraModeToggle","updateCameraModeToggle","setReturnWaypointButtonVisible","returnWaypointButton","tmpV1","pc","Vec3","tmpV2","pose","Pose","frame","InputFrame","move","rotate","applyDeadZone","stick","low","high","mag","fill","screenToWorld","camera","dz","out","aspectRatio","horizontalFov","projection","orthoHeight","app","system","height","graphicsDevice","clientRect","set","halfSize","PROJECTION_PERSPECTIVE","halfSlice","tan","math","DEG_TO_RAD","mul","CameraControls","constructor","config","this","enabled","_mode","_enableOrbit","_enableFly","enablePan","_pose","_startZoomDist","_pitchRange","Vec2","_yawRange","Infinity","_lastFocusPoint","_zoomRange","_state","axis","shift","ctrl","mouse","touches","moveSpeed","moveFastSpeed","moveSlowSpeed","rotateSpeed","rotateTouchSens","rotateJoystickSens","zoomSpeed","zoomPinchSens","keyboardSpeedMultiplier","gamepadDeadZone","invertRotation","joystickEventName","_destroyHandler","cameraComponent","_flyController","FlyController","_orbitController","OrbitController","_focusController","FocusController","moveDamping","rotateDamping","zoomDamping","zoomRange","canvas","_desktopInput","KeyboardMouseSource","_orbitMobileInput","MultiTouchSource","_flyMobileInput","DualGestureSource","_gamepadInput","GamepadSource","attach","bx","by","sx","sy","fire","look","getPosition","ZERO","_setMode","_controller","undefined","enableOrbit","enableFly","focusPoint","focusDamping","pitchRange","yawRange","mobileInputLayout","enable","damping","point","getFocus","range","copy","clamp","layout","previousMode","detach","_lastYaw","angles","setMode","focus","resetZoom","zoomDist","forward","mulScalar","sub","normalize","reset","syncFromCamera","target","clone","focusDistance","yaw","eulerAngles","getEulerAngles","syncFromPose","focusTarget","setPosition","setRotation","tempEntity","Entity","disable","read","update","dt","keyCode","button","wheel","touch","pinch","count","leftInput","rightInput","leftStick","rightStick","D","A","RIGHT","LEFT","E","Q","W","S","UP","DOWN","length","SHIFT","CTRL","fly","double","desktopPan","mobileJoystick","endsWith","moveMult","zoomMult","zoomTouchMult","rotateMult","rotateTouchMult","rotateJoystickMult","deltas","v","keyMove","panMove","wheelMove","append","mouseRotate","flyMove","orbitMove","pinchMove","orbitRotate","flyRotate","stickMove","stickRotate","xr","focusInterrupt","focusComplete","complete","yawDelta","setEulerAngles","destroy","CharacterController","velocity","isGrounded","pitch","keys","mouseLocked","sprintMultiplier","lookSensitivity","gravity","maxFallSpeed","jumpVelocity","collisionRadius","stepHeight","groundCheckDistance","horizontalVelocity","targetVelocity","collisionEntities","floorEntity","keydownHandler","keyupHandler","mousemoveHandler","clickHandler","pointerlockchangeHandler","tmpVec","tmpVec2","right","createCollisionMeshes","loadPromises","meshType","customMeshUrl","promise","loadCustomCollisionMesh","push","entity","addComponent","configureCollisionEntity","all","ext","assetType","asset","Asset","reject","ready","containerResource","resource","instantiateRenderEntity","renderEntity","children","addChild","computeAndStoreBounds","err","error","assets","load","bounds","BoundingBox","boundsInitialized","traverse","node","render","meshInstances","mi","aabb","child","_collisionBounds","halfExtents","pos","rot","scaling","setLocalScale","setEntityVisibility","_collisionMeshType","root","setupInputHandlers","removeInputHandlers","pointerLockElement","exitPointerLock","e","code","movementX","movementY","requestPointerLock","removeEventListener","checkCollision","radius","entityPos","entityScale","getLocalScale","halfWidth","halfHeight","halfDepth","customBounds","abs","checkGround","floorPos","floorScale","highestGround","topY","moveX","moveZ","isSprinting","yawRad","sin","cos","speed","lerpFactor","pow","damp","lerp","currentPos","newPos","groundY","targetFeetY","grounded","getVelocity","_GsplatRevealRadialClass","getGsplatRevealRadialClass","GsplatRevealRadial","createScript","Object","assign","prototype","effectTime","_materialsApplied","_shadersApplied","_retryCount","_maxRetries","_materialCreatedHandler","_systemMaterialHandler","_centerArray","_dotTintArray","_waveTintArray","center","acceleration","delay","dotTint","waveTint","oscillationIntensity","endRadius","initialize","Set","Color","_applyShaders","_removeShaders","floor","toFixed","size","_isEffectComplete","_updateUniforms","_setUniform","r","g","_getCompletionTime","liftStartTime","discriminant","getShaderGLSL","getShaderWGSL","gsplatComponent","gsplat","isUnified","unified","gsplatSystem","systems","material","layer","_applyShaderToMaterial","cameras","findComponents","layers","cameraComp","layerId","getLayerById","getGSplatMaterial","instance","_instance","_applyToInstance","layerList","gsplatInstance","_gsplatInstance","gsplatInst","checkEntity","inst","materials","_materials","Map","Array","isArray","_material","has","methods","obj","names","getOwnPropertyNames","getPrototypeOf","m","startsWith","join","glsl","wgsl","hasSetShaderChunk","setShaderChunk","chunks","gsplatEffectGLSL","gsplatEffectWGSL","shader","setParameter","clear","off","REVEAL_PRESETS","fast","medium","slow","getRevealPreset","preset","defineProperty","lib","loop","conditional","parse","stream","schema","result","arguments","parent","partSchema","conditionFunc","continueFunc","arr","lastStreamPos","newParent","uint8","readBits","readArray","readString","peekBytes","readBytes","readByte","buildStream","uint8Data","peekByte","offset","subarray","from","fromCharCode","readUnsigned","littleEndian","bytes","byteSize","totalOrFunc","total","parser","_byte","bits","reduce","res","def","startIndex","subBitsTotal","decompressFrames","decompressFrame","parseGIF","_gif","exports","_","require$$0","_uint","require$$1","subBlocksSchema","blocks","streamSize","availableSize","Uint8Array","gceSchema","gce","codes","extras","future","disposal","userInput","transparentColorGiven","transparentColorIndex","terminator","imageSchema","image","descriptor","left","top","lct","exists","interlaced","sort","minCodeSize","textSchema","blockSize","preData","applicationSchema","application","commentSchema","comment","_default","header","signature","version","lsd","gct","resolution","backgroundColorIndex","pixelAspectRatio","frames","nextCode","__esModule","default","_jsBinarySchemaParser","require$$2","_deinterlace","deinterlace_1","deinterlace","pixels","newPixels","rows","cpRow","toRow","fromRow","fromPixels","slice","splice","apply","concat","offsets","steps","pass","_lzw","lzw_1","lzw","pixelCount","available","code_mask","code_size","end_of_information","in_code","old_code","data_size","datum","first","pi","bi","MAX_STACK_SIZE","npix","dstPixels","prefix","suffix","pixelStack","arrayBuffer","byteData","buildImagePatch","totalPixels","resultImage","dims","colorTable","disposalType","transparentIndex","patch","patchData","Uint8ClampedArray","colorIndex","generatePatch","parsedGif","buildImagePatches","f","AnimatedGifTexture","currentFrameIndex","isLoaded","lastFrameTime","updateHandler","texture","gifWidth","gifHeight","drawFrame","ctx","getContext","willReadFrequently","buffer","gif","Texture","format","PIXELFORMAT_RGBA8","mipmaps","minFilter","FILTER_LINEAR","magFilter","addressU","ADDRESS_CLAMP_TO_EDGE","addressV","onReady","onError","frameIndex","prevFrame","clearRect","imageData","ImageData","tempCanvas","putImageData","drawImage","updateTexture","getImageData","lock","unlock","upload","stop","playing","loaded","HtmlMeshManager","meshes","useTexElement2D","GraphicsDevice","_isHTMLElementInterface","HTMLImageElement","HTMLCanvasElement","HTMLVideoElement","originalIsBrowserInterface","_isBrowserInterface","call","patchPlayCanvasForHtmlMesh","device","supportsTexElement2D","createMesh","htmlElement","createHtmlElement","createTexture","createMaterial","createEntity","destroyMesh","updateMeshTexture","animated","startUpdateLoop","pointerEvents","zIndex","overflow","css","html","setAttribute","visibility","body","setSource","renderToCanvas","svg","img","Image","blob","Blob","URL","createObjectURL","revokeObjectURL","onerror","fillStyle","fillRect","font","textAlign","fillText","StandardMaterial","diffuseMap","emissiveMap","emissive","opacity","blendType","BLEND_NORMAL","BLEND_NONE","cull","doubleSided","CULLFACE_NONE","CULLFACE_BACK","aspect","castShadows","receiveShadows","billboard","findComponent","lookAt","lastUpdate","rate","updateRate","last","delete","values","some","getMesh","updateVisibility","scrollPercent","waypointIndex","visibilityRange","start","end","billboardRange","billboardActive","_billboardActive","getAllMeshes","CustomScriptSystem","isInitialized","scriptCleanup","lastError","updateCallbacks","api","registerCleanup","fn","addCleanup","updateScript","execute","sanitizeScript","s","preprocessScript","processed","trim","cleanup","processedScript","cancelled","watchdog","requestAnimationFrame","wrappedApp","create","registerUpdate","callback","safeCallback","idx","indexOf","registerBeforeRender","pcNamespace","getScrollPercentage","getCurrentWaypointIndex","getHotspots","getSplats","getHTMLMeshes","func","Function","module","fakeRequire","blocked","Proxy","cleanupCandidate","bind","getLastError","dispose","EventEmitter","listeners","event","emit","args","cb","isMobileDevice","vendor","window","opera","LOD_PRESETS","lodDistances","desktop","mobile","isLodStreamingFormat","createViewer","lazyLoad","lazyEvents","actualInstance","lazyLoadThumbnail","buttonText","lazyLoadButtonText","onStart","lazyLoadContainer","startBtn","transition","createLazyLoadUI","goToWaypoint","getWaypointCount","getRotation","el","resize","navigateToScene","events","showUI","uiOpts","mapCameraMode","hasCollisionMeshesForWalk","allowedModes","a","defaultMode","uiElements","showScrollControls","showModeToggle","showFullscreenButton","showHelpButton","showPreloader","fullscreenBtn","vrBtn","vrButton","arBtn","arButton","helpBtn","hotspotPopup","watermark","finalLink","createUIElements","baseGraphicsOptions","antialias","alpha","powerPreference","Application","graphicsDeviceOptions","Mouse","TouchDevice","keyboard","Keyboard","webgl2Error","preferWebGl2","webgl1Error","errorDiv","heading","message","preventDefault","setCanvasFillMode","FILLMODE_FILL_WINDOW","setCanvasResolution","RESOLUTION_AUTO","isMobile","lodPresetName","lodPreset","lodUpdateAngle","lodBehindPenalty","radialSorting","lodUpdateDistance","lodUnderfillLimit","lodRangeMin","lodRangeMax","colorUpdateDistance","colorUpdateAngle","colorUpdateDistanceLodScale","colorUpdateAngleLodScale","lodRange","currentWaypointIndex","splatEntity","revealScript","isDestroyed","currentSplatUrl","isLoadingSplat","preloadedSplats","lastSplatCheckProgress","lastSplatCheckWaypointIndex","clearColor","hex","parseInt","substring","finalRot","convertWaypointRotation","light","LIGHTTYPE_DIRECTIONAL","intensity","cameraControls","characterController","then","catch","currentCameraMode","waypointControlEnabled","picker","Picker","isInXR","xrSessionType","userYawOffset","userPitchOffset","isUserDragging","targetCameraPosition","targetCameraRotation","pickerScale","canvasEl","clientWidth","clientHeight","worldLayer","getLayerByName","prepare","centerX","centerY","worldPoint","getWorldPointAsync","findBetterFocusPoint","cameraPos","hotspotEntities","hotspotData","videoElement","mediaTriggerMode","hotspotPos","proximityDistance","isVideoPlaying","playVideoHotspot","pauseVideoHotspot","updateProximityTriggers","waypointAudioMap","audioData","audioId","slotId","assetReady","slot","sound","spatialSound","audioPos","maxDistance","stopOnExit","updateWaypointAudioProximity","wpPos","triggerDist","activeWaypointTriggers","interaction","executeWaypointInteractions","reverseWaypointInteractions","checkWaypointTriggerDistance","currentMode","updateProgress","bar","textEl","percent","updatePreloaderProgress","qx","_x","qy","_y","qz","_z","qw","_w","w","Quat","setFromEulerAngles","hideSplat","disposeSplat","preloadNextSplat","currentIndex","nextIndex","nextSplat","Date","invertX","invertY","finalScale","finalRotDeg","preloadSplat","loadSwapSplat","currentEntity","showSplat","swapIndex","findIndex","updateSplats","numWaypoints","currentProgress","bestWaypointSplat","bestPercentageSplat","bestWaypointTrigger","bestPercentageTrigger","splat","bestSplat","primaryUrl","targetUrl","skyboxUrl","skyboxAsset","TEXTURETYPE_RGBM","skyboxMip","rotQuat","skyboxRotation","applySkybox","totalDuration","sum","playbackSpeed","rawLoopMode","playbackDirection","lastPointerX","lastPointerY","waypointPositions","waypointRotations","waypointFOVs","defaultFOV","targetFOV","updateCameraFromProgress","segmentProgress","segmentIndex","t","startPos","endPos","startRot","endRot","slerp","startFOV","endFOV","newIndex","prevIndex","currentWaypoint","waypointCameraMode","orbitTarget","previousIndex","autoplayTriggered","isLeaving","autoplay","setProgress","animate","animateToProgress","newFOV","userRotationOffset","targetWithUserOffset","mul2","currentRot","newRot","transitionDuration","targetProgress","startProgress","startTime","elapsed","buttonMode","increment","lastPlaybackTime","playbackAnimationId","playbackLoop","timestamp","deltaTime","cancelAnimationFrame","scrollCanvas","scrollSpeedSetting","baseIncrement","scrollIncrement","deltaY","passive","clientX","clientY","capture","deltaX","portalEntities","parseColor","allAudioContexts","particleEntities","particleTextures","PARTICLE_TEXTURE_URLS","flare","circle","spark","rain","smoke","loadParticleTexture","createParticleSystemEntity","psConfig","getPos","vec","defaultVal","getColor","emitterPos","emitterPosition","color1","color2","colorDead","direction1","direction2","lifetime","minLifeTime","maxLifeTime","emitterShape","EMITTERSHAPE_BOX","emitterExtents","emitterOffset","emitterType","EMITTERSHAPE_SPHERE","emitterRadius","emitBoxMin","emitBoxMax","boxMin","boxMax","blendMode","BLEND_ADDITIVEALPHA","blendModeStr","BLEND_MULTIPLICATIVE","BLEND_ADDITIVE","gravityX","gravityY","gravityZ","endVelX","endVelY","endVelZ","minAngularSpeed","maxAngularSpeed","angularSpeed","minInitialRotation","maxInitialRotation","minSize","maxSize","minScaleX","maxScaleX","minScaleY","maxScaleY","minEmitPower","maxEmitPower","avgDirX","avgDirY","avgDirZ","numParticles","emitRate","startAngle","startAngle2","radialSpeedGraph","Curve","localVelocityGraph","CurveSet","localVelocityGraph2","velocityGraph","scaleGraph","scaleGraph2","rotationSpeedGraph","rotationSpeedGraph2","colorGraph","alphaGraph","blend","depthWrite","depthSoftening","softParticles","lighting","halfLambert","alignToMotion","stretch","preWarm","orientation","particlesystem","localSpace","customMeshEntities","playAnimComponentV2","animInfo","component","animations","clips","modelEntity","animList","firstAnim","_name","_duration","anim","stateGraphData","states","transitions","to","time","conditions","activate","loadStateGraph","animAsset","assignAnimation","animComponent","loopedTime","_curves","curve","paths","_paths","entityPath","findByPath","findByName","evaluate","prop","propertyPath","setLocalPosition","setLocalRotation","baseLayer","playableState","pauseAnimComponentV2","loadCustomMesh","meshConfig","modelUrl","radToDeg","modelAsset","meshId","meshData","isAnimPlaying","audioPlaying","enableAllChildren","childCount","opacityMode","applyOpacityToCustomMesh","originalRotation","camPos","meshPos","angle","atan2","hasGlbAnimations","hasAnimations","animationCount","interactionConfig","playModelAnimation","allAnimComponents","findAllAnimComponents","depth","animation","animationNames","currentTime","shouldAutoPlay","animationAutoPlay","playAudio","audioUrl","audioConfig","positional","audioSpatial","distanceModel","audioDistanceModel","refDistance","audioRefDistance","audioMaxDistance","rollOffFactor","audioRollOffFactor","slots","audioLoop","volume","audioVolume","audioSlotId","audioAsset","setupMeshAudio","customMesh","triggerUIPopup","triggerDirectLink","hasInteractions","canvasForMesh","performRaycast","rect","getBoundingClientRect","dir","sub2","rayIntersectsMeshHierarchy","dot","add2","popupTriggerMode","audioTriggerMode","animationTriggerMode","directLinkTriggerMode","isHovering","handleHoverIn","cursor","displayMeshContent","handleDirectLink","handleHoverOut","meshPopup","hideMeshContent","handleClick","meshClickHandler","meshHoverHandler","nowHovering","meshLeaveHandler","setupMeshClickInteraction","opacityConfig","rayOrigin","rayDir","rayIntersectsAABB","model","getMin","getMax","tmin","tmax","origin","minVal","maxVal","t1","t2","existingPopup","popupContent","content","onclick","showMeshPopup","directLinkUrl","open","applyToEntity","ent","meshInstance","_isCloned","BLEND_PREMULTIPLIED","depthTest","alphaTest","cleanupCustomMeshes","canvasForCleanup","hexToColorForLights","exec","initCustomLights","lightConfig","LIGHTTYPE_POINT","createPointLight","createDirectionalLightEntity","groundColor","skyColor","ambientLight","createHemisphericLight","createAmbientLight","angleDeg","exponent","innerAngleDeg","innerConeAngle","innerAngle","outerAngleDeg","outerConeAngle","outerAngle","LIGHTTYPE_SPOT","shadowBias","normalOffsetBias","direction","dirVec","createSpotLight","getPlayCanvasDistanceModel","modelStr","opacityAnimation","startPercent","endPercent","startOpacity","endOpacity","bRange","updateCustomMeshVisibility","animatedGifs","createImageMaterial","imageUrl","useLighting","onTextureReady","twoSidedLighting","diffuse","specular","lower","isGifUrl","gifTexture","opacityMap","crossOrigin","FILTER_LINEAR_MIPMAP_LINEAR","createHotspots","sz","rotX","rotY","rotZ","sphereOpacity","initialOpacity","targetOpacity","textureLoaded","hiddenUntilTextureLoaded","shouldBeVisible","rotateLocal","hotspotMaterial","videoUrl","useAlphaMethod","useIOSVideoAlphaMethod","forceIOSVideoAlphaMethodForAllDevices","mainVideoUrl","iosMainVideoUrl","alphaVideoUrl","alphaMaskVideoUrl","hasWebMAlpha","isWebMUrl","webmHasAlpha","createVideoAndTexture","isAlpha","useRGBA","videoLoop","playsInline","muted","videoMuted","PIXELFORMAT_R8_G8_B8_A8","PIXELFORMAT_R8_G8_B8","video","main","videoTexture","alphaVideo","alphaTexture","opacityMapChannel","paused","readyState","HAVE_ENOUGH_DATA","initialScale","videoWidth","videoHeight","ratio","alphaVideoElement","videoSpatialAudio","audioCtx","AudioContext","webkitAudioContext","source","createMediaElementSource","panner","createPanner","panningModel","videoDistanceModel","videoRefDistance","videoMaxDistance","rolloffFactor","videoRolloffFactor","connect","destination","camForward","camUp","up","listener","positionX","positionY","positionZ","forwardX","forwardY","forwardZ","upX","upY","upZ","setOrientation","setupVideoSpatialAudio","gifUrl","loadAnimatedGif","gifCanvas","animateGif","gifAnimationFrame","fallbackMaterial","audioElements","audio","audioRolloffFactor","updateAudioPosition","setupHotspotAudio","hasBillboardRange","billboardRangeStart","billboardRangeEnd","_billboardOriginalRotation","updateHotspotVisibility","rangeStart","rangeEnd","origRot","triggerMode","state","resume","updatePortalVisibility","portal","portalData","portalPos","proximityTriggered","targetSceneName","targetSceneId","handlePortalNavigation","raycastPortals","closestHit","portalId","sceneName","existing","loadingDiv","spinner","background","flexDirection","alignItems","justifyContent","border","borderTop","borderRadius","marginBottom","styleEl","showPortalLoadingUI","sceneApiUrl","status","newSceneData","__htmlMeshManager","__customScriptSystem","cleanupForPortalNavigation","parentNode","newViewer","hidePortalLoadingUI","padding","raycastHotspots","currentHoverHotspot","isMouseOverPopup","overlay","handleDoubleClickFocus","screenX","screenY","hotspotHit","scaledX","scaledY","getSelectionAsync","aabbCenter","portalHit","effectiveActivationMode","hit","popupEl","overlayEl","hitResult","hasPopupContent","teleportWaypoint","teleportToWaypoint","teleportPercent","teleportToPercent","teleportMode","calculatedTargetProgress","targetIndex","lastTapTime","changedTouches","urls","received","hasRejected","unhandledRejectionHandler","errorMsg","reason","details","gs","hasUserRotation","revealPresetName","revealEffect","revealConfig","GsplatRevealRadialClass","syncError","statusCode","stack","loadSplat","portalMaterial","audioEntity","soundConfig","audioDataRef","ps","textureName","particleTexture","textureUrl","textureCacheKey","customTextureUrl","colorMap","safeName","initParticleSystems","loadedCount","skippedCount","hasModelUrl","initCustomMeshes","skyboxIntensity","enableIBL","isHDR","skyboxEntity","skyboxMaterial","CULLFACE_FRONT","textureAsset","exposure","toneMapping","TONEMAP_ACES","iblError","rotationDegrees","initSkybox","isAvailable","XRTYPE_VR","XRTYPE_AR","startXr","XRSPACE_LOCALFLOOR","setupXR","htmlMeshManager","manager","setupHtmlMeshes","customScriptSystem","scriptSystem","setupCustomScript","urlParams","URLSearchParams","location","search","waypointParam","autoplayParam","isNaN","nearestIndex","nearestDistance","handleResize","resizeCanvas","createViewerFromUrl","SceneNotFoundError","super","SceneApiError","DEFAULT_BASE_URL","process","env","STORYSPLAT_API_URL","__STORYSPLAT_API_URL__","createViewerFromSceneId","baseUrl","apiUrl","encodeURIComponent","headers","apiKey","method","errorText","errorMessage","apiResponse","success","meta","_baseUrl","_apiKey","viewerOptions","fetchSceneMeta","userSlug"],"mappings":"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","x_google_ignoreList":[7,8,9,10,11,12]}
|