369 lines
No EOL
11 KiB
JavaScript
369 lines
No EOL
11 KiB
JavaScript
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 <quackscape-scene> 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 = `
|
|
<div class="loading-animation">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
<div class="loading-text">Loading scene...</div>
|
|
`;
|
|
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 = `
|
|
<div class="error-icon">
|
|
<i class="ph-light ph-warning"></i>
|
|
</div>
|
|
<div class="error-text">Failed to load scene</div>
|
|
<div class="error-details">${error.message || "Unknown error"}</div>
|
|
<button class="btn btn-primary retry-btn">Retry</button>
|
|
`;
|
|
|
|
// 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 }; |