import { getScene } from "./api"; import { loadSidebar } from "./scene/sidebar"; import { loadNavbar } from "./scene/navbar"; import aframe from "aframe"; import 'bootstrap/dist/js/bootstrap.bundle.min.js'; import "@phosphor-icons/web/light"; import "../scss/frontend.scss"; import "../css/scene.css"; // Scene loading state tracking const loadingStates = new Map(); AFRAME.registerComponent('billboard', { tick: function () { const camera = document.querySelector('a-camera'); if (!camera) return; // Make this entity face the camera this.el.object3D.lookAt(camera.object3D.position); } }); // Define the element class QuackscapeScene extends HTMLElement { connectedCallback() { this.scene = this.getAttribute("scene"); this.embedded = this.hasAttribute("embedded"); this.x = parseInt(this.getAttribute("x")) || 0; this.y = parseInt(this.getAttribute("y")) || 0; this.z = parseInt(this.getAttribute("z")) || 0; this.navbar = this.hasAttribute("navbar"); this.sidebar = this.hasAttribute("sidebar"); // Show loading indicator this.showLoadingIndicator(); // Load the scene loadScene(this.scene, this.x, this.y, this.z, this, this.embedded) .then(() => { // Scene loaded successfully this.hideLoadingIndicator(); // Dispatch a loaded event const loadedEvent = new CustomEvent("sceneLoaded", { detail: { sceneId: this.scene } }); this.dispatchEvent(loadedEvent); }) .catch(error => { console.error("Error loading scene:", error); this.showErrorIndicator(error); }); // Load UI components if needed if (this.navbar) { loadNavbar(this.scene, this); } else if (this.sidebar) { loadSidebar(this.scene, this); } } showLoadingIndicator() { // Create and show loading indicator const loadingEl = document.createElement("div"); loadingEl.className = "scene-loading"; loadingEl.innerHTML = `
Loading scene...
`; this.appendChild(loadingEl); this.loadingIndicator = loadingEl; } hideLoadingIndicator() { if (this.loadingIndicator) { this.loadingIndicator.classList.add("loaded"); // Remove after animation completes setTimeout(() => { if (this.loadingIndicator && this.loadingIndicator.parentNode) { this.loadingIndicator.parentNode.removeChild(this.loadingIndicator); } }, 500); } } showErrorIndicator(error) { if (this.loadingIndicator) { this.loadingIndicator.innerHTML = `
Failed to load scene
${error.message || "Unknown error"}
`; // Add retry functionality const retryBtn = this.loadingIndicator.querySelector(".retry-btn"); if (retryBtn) { retryBtn.addEventListener("click", () => { this.hideLoadingIndicator(); this.showLoadingIndicator(); loadScene(this.scene, this.x, this.y, this.z, this, this.embedded) .then(() => this.hideLoadingIndicator()) .catch(err => this.showErrorIndicator(err)); }); } } } } // Register the custom element customElements.define("quackscape-scene", QuackscapeScene); // Prevent context menu in scene for better mobile experience document.addEventListener("contextmenu", function (event) { const isScene = event.target.closest("a-scene"); if (isScene) { event.preventDefault(); } }); // Function to load a scene into a destination object async function loadScene( scene_id, x = 0, y = 0, z = 0, destination = null, embedded = false ) { // Check if we're already loading this scene if (loadingStates.has(scene_id) && loadingStates.get(scene_id).loading) { return loadingStates.get(scene_id).promise; } // Create a promise to track loading state let resolvePromise, rejectPromise; const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); // Set loading state loadingStates.set(scene_id, { loading: true, promise }); try { // Get WebGL maximum texture size const gl = getWebGLContext(); if (!gl) { throw new Error("WebGL not supported by your browser"); } const maxTextureSize = getMaxTextureSize(gl); // Get scene information from API const response = await getScene(scene_id); const scene = response.obj; // Find appropriate resolution const content = findBestResolution(scene.base_content.resolutions, maxTextureSize); if (!content) { throw new Error("No suitable resolution found for this device"); } // Select a destination element if not specified if (!destination) { destination = document.querySelector("quackscape-scene") || document.body; } destination.setAttribute("id", scene_id); // Remove any existing scenes const existingScene = destination.querySelector("a-scene"); if (existingScene) { existingScene.remove(); } // Create the A-Frame scene const a_scene = createAFrameScene(embedded); // Create camera rig with proper rotation const rig = createCameraRig(x, y, z); a_scene.appendChild(rig); // Create assets container const assets = document.createElement("a-assets"); // Add background image const background = document.createElement("img"); background.setAttribute("id", content.id); background.setAttribute("src", content.file); background.setAttribute("crossorigin", "anonymous"); // Add loading event handlers background.addEventListener("load", () => { console.log("Background image loaded successfully"); }); background.addEventListener("error", (e) => { console.error("Error loading background image:", e); rejectPromise(new Error("Failed to load background image")); }); assets.appendChild(background); a_scene.appendChild(assets); // Add sky with background image const sky = document.createElement("a-sky"); sky.setAttribute("src", `#${content.id}`); sky.setAttribute("rotation", "0 0 0"); sky.setAttribute("data-clickable", ""); a_scene.appendChild(sky); // Add elements to scene scene.elements.forEach((element) => { const node = document.createElement(element.data.tag); node.setAttribute("id", element.data.id); for (const [key, value] of Object.entries(element.data.attributes)) { node.setAttribute(key, value); } // Add children elements if they exist if (element.data.children && Array.isArray(element.data.children)) { element.data.children.forEach(child => { const childNode = document.createElement(child.tag); // Set attributes on child for (const [key, value] of Object.entries(child.attributes || {})) { childNode.setAttribute(key, value); } node.appendChild(childNode); }); } a_scene.appendChild(node); }); // Add scene to destination destination.appendChild(a_scene); // Wait for scene to load a_scene.addEventListener("loaded", function () { console.log("A-Frame scene loaded"); // Update scene title in overlay if we have stored title if (window.selectedSceneTitle) { const titleOverlay = document.getElementById("sceneTitleOverlay"); if (titleOverlay) { const titleElement = titleOverlay.querySelector("h1"); const categoryElement = titleOverlay.querySelector("p"); if (titleElement) { titleElement.textContent = window.selectedSceneTitle; } if (categoryElement && window.selectedSceneCategory) { categoryElement.textContent = window.selectedSceneCategory; } // Show title overlay titleOverlay.classList.add("visible"); // Hide after delay setTimeout(() => { titleOverlay.classList.remove("visible"); }, 3000); // Clear stored title after using it window.selectedSceneTitle = null; window.selectedSceneCategory = null; } } // Dispatch a signal for the editor to pick up const loaded_event = new CustomEvent("loadedQuackscapeScene", { detail: { sceneId: scene_id } }); document.dispatchEvent(loaded_event); // Update loading state loadingStates.set(scene_id, { loading: false }); resolvePromise(); }); return promise; } catch (error) { console.error("Error in loadScene:", error); loadingStates.set(scene_id, { loading: false }); rejectPromise(error); throw error; } } // Helper functions function getWebGLContext() { const canvas = document.createElement("canvas"); return canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); } function getMaxTextureSize(gl) { let maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); // iOS devices have issues with very large textures const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent); if (iOS) { maxTextureSize = Math.min(8192, maxTextureSize); } return maxTextureSize; } function findBestResolution(resolutions, maxTextureSize) { // Sort resolutions by width (largest first) const sortedResolutions = [...resolutions].sort((a, b) => b.width - a.width); // Find the largest resolution that fits within maxTextureSize return sortedResolutions.find(resolution => resolution.width <= maxTextureSize) || sortedResolutions[sortedResolutions.length - 1]; // Fallback to smallest if none fit } function createAFrameScene(embedded) { const a_scene = document.createElement("a-scene"); a_scene.setAttribute("cursor", "rayOrigin: mouse"); a_scene.setAttribute("raycaster", "objects: [data-clickable]; far: 100"); if (embedded) { a_scene.setAttribute("embedded", "true"); a_scene.setAttribute("vr-mode-ui", "enabled: false"); } // Add loading screen a_scene.setAttribute("loading-screen", "dotsColor: #4F46E5; backgroundColor: #FFFFFF"); return a_scene; } function createCameraRig(x, y, z) { const rig = document.createElement("a-entity"); rig.setAttribute("id", "rig"); // Set rotation if specified if (x !== -1 && y !== -1 && z !== -1) { rig.setAttribute("rotation", `${x} ${y} ${z}`); } const camera = document.createElement("a-camera"); camera.setAttribute("wasd-controls-enabled", "false"); camera.setAttribute("look-controls", "reverseMouseDrag: false"); rig.appendChild(camera); return rig; } // Make functions available globally window.loadScene = loadScene; window.aframe = aframe; export { loadScene };