2313 lines
No EOL
72 KiB
JavaScript
2313 lines
No EOL
72 KiB
JavaScript
import { getScene, getSceneElement, getCategory, getCookie } from "./api";
|
|
import { populateDestinationDropdown } from "./editor/teleport";
|
|
import { showNotification } from "./utils/notifications";
|
|
|
|
import registerClickDrag from "@kumitterer/aframe-click-drag-component";
|
|
import "@phosphor-icons/web/light";
|
|
|
|
import "../css/editor.css";
|
|
|
|
// Register A-Frame components
|
|
registerClickDrag(window.aframe);
|
|
|
|
// Editor state
|
|
const editorState = {
|
|
activeElement: null,
|
|
editMode: "select", // select, marker, image, teleport
|
|
isDragging: false,
|
|
clickTimestamp: 0,
|
|
scene: null,
|
|
modified: false,
|
|
originalData: null,
|
|
currentPosition: null // Used to track the position of a dragged element
|
|
};
|
|
|
|
// Global variables for drag state
|
|
const dragState = {
|
|
isDragging: false,
|
|
element: null,
|
|
lastMouseX: 0,
|
|
lastMouseY: 0,
|
|
radius: 5
|
|
};
|
|
|
|
// Initialize editor when DOM is loaded
|
|
document.addEventListener("DOMContentLoaded", initEditor);
|
|
|
|
// Initialize editor
|
|
function initEditor() {
|
|
console.log("Initializing editor...");
|
|
|
|
// Set up tool buttons
|
|
setupToolButtons();
|
|
|
|
// Set up save button
|
|
const saveBtn = document.getElementById("saveBtn");
|
|
if (saveBtn) {
|
|
saveBtn.addEventListener("click", saveChanges);
|
|
} else {
|
|
console.warn("Save button not found");
|
|
}
|
|
|
|
// Set up preview button
|
|
const previewBtn = document.getElementById("previewBtn");
|
|
if (previewBtn) {
|
|
previewBtn.addEventListener("click", previewScene);
|
|
} else {
|
|
console.warn("Preview button not found");
|
|
}
|
|
|
|
// Set up reset view button
|
|
const resetViewBtn = document.getElementById("resetViewBtn");
|
|
if (resetViewBtn) {
|
|
resetViewBtn.addEventListener("click", resetView);
|
|
} else {
|
|
console.warn("Reset view button not found");
|
|
}
|
|
|
|
// Set up fullscreen button
|
|
const fullscreenBtn = document.getElementById("fullscreenBtn");
|
|
if (fullscreenBtn) {
|
|
fullscreenBtn.addEventListener("click", toggleFullscreen);
|
|
} else {
|
|
console.warn("Fullscreen button not found");
|
|
}
|
|
|
|
// Set up settings form submission
|
|
const saveSettingsBtn = document.getElementById("saveSettingsBtn");
|
|
if (saveSettingsBtn) {
|
|
saveSettingsBtn.addEventListener("click", saveSceneSettings);
|
|
} else {
|
|
console.warn("Save settings button not found");
|
|
}
|
|
|
|
// Set up Add Element dropdown
|
|
setupAddElementDropdown();
|
|
|
|
// Listen for scene loaded event
|
|
document.addEventListener("loadedQuackscapeScene", handleSceneLoaded);
|
|
|
|
// Listen for tab changes
|
|
setupTabListeners();
|
|
|
|
// Set up custom position handling for direct input
|
|
setupDirectPositionHandling();
|
|
|
|
// Set up before unload warning
|
|
window.addEventListener("beforeunload", function (e) {
|
|
if (editorState.modified) {
|
|
const confirmationMessage = "You have unsaved changes. Are you sure you want to leave?";
|
|
e.returnValue = confirmationMessage;
|
|
return confirmationMessage;
|
|
}
|
|
});
|
|
|
|
// Set up "Use Current View" button
|
|
const useCurrentViewBtn = document.getElementById("useCurrentViewBtn");
|
|
if (useCurrentViewBtn) {
|
|
useCurrentViewBtn.addEventListener("click", useCurrentViewAsDefault);
|
|
} else {
|
|
console.warn("Use Current View button not found");
|
|
}
|
|
|
|
console.log("Editor initialization complete");
|
|
}
|
|
|
|
// Set the current camera orientation as the default view
|
|
function useCurrentViewAsDefault() {
|
|
console.log("Setting current view as default");
|
|
|
|
// Get the camera element
|
|
const camera = document.querySelector("a-camera");
|
|
if (!camera) {
|
|
console.error("Camera element not found");
|
|
showNotification("Camera not found", "error");
|
|
return;
|
|
}
|
|
|
|
// Get the camera rig (parent of camera)
|
|
const rig = camera.parentElement;
|
|
if (!rig) {
|
|
console.error("Camera rig not found");
|
|
showNotification("Camera rig not found", "error");
|
|
return;
|
|
}
|
|
|
|
// Debug: Log all available components and properties
|
|
console.log("Camera components:", camera.components);
|
|
console.log("Rig components:", rig.components);
|
|
console.log("Rig attributes:", rig.attributes);
|
|
console.log("Camera object3D:", camera.object3D);
|
|
console.log("Rig object3D:", rig.object3D);
|
|
|
|
// Try to get rotation from look-controls component
|
|
let x = 0, y = 0, z = 0;
|
|
|
|
if (camera.components && camera.components['look-controls']) {
|
|
const lookControls = camera.components['look-controls'];
|
|
console.log("Look controls:", lookControls);
|
|
|
|
// Look controls typically store pitch (x) and yaw (y)
|
|
if (lookControls.pitchObject && lookControls.yawObject) {
|
|
x = THREE.MathUtils.radToDeg(lookControls.pitchObject.rotation.x);
|
|
y = THREE.MathUtils.radToDeg(lookControls.yawObject.rotation.y);
|
|
z = 0; // Z rotation is typically not used in look-controls
|
|
|
|
console.log("Look controls rotation:", { x, y, z });
|
|
}
|
|
}
|
|
|
|
// If we couldn't get values from look-controls, try the rig's rotation attribute
|
|
if (x === 0 && y === 0 && z === 0) {
|
|
// Get current rotation from the rig element
|
|
const rotation = rig.getAttribute("rotation");
|
|
if (rotation) {
|
|
x = rotation.x;
|
|
y = rotation.y;
|
|
z = rotation.z;
|
|
console.log("Rig rotation attribute:", rotation);
|
|
}
|
|
}
|
|
|
|
// If we still don't have values, try the object3D directly
|
|
if (x === 0 && y === 0 && z === 0) {
|
|
if (rig.object3D) {
|
|
// Force the object3D to update its matrix
|
|
rig.object3D.updateMatrixWorld(true);
|
|
|
|
// Get Euler angles from the world matrix
|
|
const euler = new THREE.Euler().setFromRotationMatrix(rig.object3D.matrixWorld);
|
|
|
|
x = THREE.MathUtils.radToDeg(euler.x);
|
|
y = THREE.MathUtils.radToDeg(euler.y);
|
|
z = THREE.MathUtils.radToDeg(euler.z);
|
|
|
|
console.log("Object3D world rotation:", { x, y, z });
|
|
}
|
|
}
|
|
|
|
// As a last resort, try to get camera position and calculate rotation
|
|
if (x === 0 && y === 0 && z === 0) {
|
|
// This is a simplified approach - in a real VR scene, you might need more complex calculations
|
|
console.log("Using last resort method to calculate rotation");
|
|
|
|
// Get the current camera direction by raycasting forward
|
|
const raycaster = new THREE.Raycaster();
|
|
camera.object3D.updateMatrixWorld(true);
|
|
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera.object3D.children[0]);
|
|
|
|
// Calculate rotation based on the ray direction
|
|
const direction = raycaster.ray.direction;
|
|
y = THREE.MathUtils.radToDeg(Math.atan2(direction.x, direction.z));
|
|
x = THREE.MathUtils.radToDeg(Math.atan2(-direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)));
|
|
|
|
console.log("Calculated rotation from direction:", { x, y, z });
|
|
}
|
|
|
|
// Update the input fields with the current rotation values
|
|
const defaultXInput = document.getElementById("defaultX");
|
|
const defaultYInput = document.getElementById("defaultY");
|
|
const defaultZInput = document.getElementById("defaultZ");
|
|
|
|
if (defaultXInput) defaultXInput.value = parseFloat(x).toFixed(2);
|
|
if (defaultYInput) defaultYInput.value = parseFloat(y).toFixed(2);
|
|
if (defaultZInput) defaultZInput.value = parseFloat(z).toFixed(2);
|
|
|
|
// Mark the scene as modified
|
|
editorState.modified = true;
|
|
|
|
// Show success notification
|
|
showNotification("Current view set as default", "success");
|
|
|
|
// Highlight the input fields briefly to show they've been updated
|
|
[defaultXInput, defaultYInput, defaultZInput].forEach(input => {
|
|
if (input) {
|
|
input.classList.add("highlight-update");
|
|
setTimeout(() => {
|
|
input.classList.remove("highlight-update");
|
|
}, 1000);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to update preview from input values
|
|
function updatePreviewFromInputs() {
|
|
const previewElement = document.querySelector(".element-preview");
|
|
if (!previewElement) return;
|
|
|
|
const x = parseFloat(document.getElementById("position_x").value) || 0;
|
|
const y = parseFloat(document.getElementById("position_y").value) || 0;
|
|
const z = parseFloat(document.getElementById("position_z").value) || 0;
|
|
|
|
console.log("Updating preview position from inputs:", x, y, z);
|
|
previewElement.setAttribute("position", `${x} ${y} ${z}`);
|
|
}
|
|
|
|
// Custom position handling for direct input
|
|
function setupDirectPositionHandling() {
|
|
console.log("Setting up direct position handling");
|
|
|
|
// Listen for changes to position inputs
|
|
document.addEventListener("input", function (event) {
|
|
if (event.target.id === "position_x" ||
|
|
event.target.id === "position_y" ||
|
|
event.target.id === "position_z") {
|
|
|
|
updatePreviewFromInputs();
|
|
}
|
|
});
|
|
|
|
// Override the A-Frame click-drag behavior
|
|
document.addEventListener("mouseup", function (event) {
|
|
const previewElement = document.querySelector(".element-preview");
|
|
if (!previewElement) return;
|
|
|
|
// Get the current position after drag
|
|
const position = previewElement.getAttribute("position");
|
|
if (!position) return;
|
|
|
|
// Update form inputs
|
|
const posXInput = document.getElementById("position_x");
|
|
const posYInput = document.getElementById("position_y");
|
|
const posZInput = document.getElementById("position_z");
|
|
|
|
if (posXInput) posXInput.value = position.x.toFixed(2);
|
|
if (posYInput) posYInput.value = position.y.toFixed(2);
|
|
if (posZInput) posZInput.value = position.z.toFixed(2);
|
|
|
|
console.log("Updated inputs after drag:", position);
|
|
});
|
|
}
|
|
|
|
// Set up tool buttons
|
|
function setupToolButtons() {
|
|
const selectTool = document.getElementById("selectTool");
|
|
const markerTool = document.getElementById("markerTool");
|
|
const imageTool = document.getElementById("imageTool");
|
|
const teleportTool = document.getElementById("teleportTool");
|
|
|
|
if (selectTool) {
|
|
selectTool.addEventListener("click", () => setEditMode("select"));
|
|
} else {
|
|
console.warn("Select tool button not found");
|
|
}
|
|
|
|
if (markerTool) {
|
|
markerTool.addEventListener("click", () => setEditMode("marker"));
|
|
} else {
|
|
console.warn("Marker tool button not found");
|
|
}
|
|
|
|
if (imageTool) {
|
|
imageTool.addEventListener("click", () => setEditMode("image"));
|
|
} else {
|
|
console.warn("Image tool button not found");
|
|
}
|
|
|
|
if (teleportTool) {
|
|
teleportTool.addEventListener("click", () => setEditMode("teleport"));
|
|
} else {
|
|
console.warn("Teleport tool button not found");
|
|
}
|
|
}
|
|
|
|
// Set up Add Element dropdown
|
|
function setupAddElementDropdown() {
|
|
// Set up Add Element buttons
|
|
const addMarkerBtn = document.getElementById("addMarkerBtn");
|
|
if (addMarkerBtn) {
|
|
addMarkerBtn.addEventListener("click", function () {
|
|
setEditMode("marker");
|
|
simulateClickOnSky();
|
|
});
|
|
} else {
|
|
console.warn("Add marker button not found");
|
|
}
|
|
|
|
const addImageBtn = document.getElementById("addImageBtn");
|
|
if (addImageBtn) {
|
|
addImageBtn.addEventListener("click", function () {
|
|
setEditMode("image");
|
|
simulateClickOnSky();
|
|
});
|
|
} else {
|
|
console.warn("Add image button not found");
|
|
}
|
|
|
|
const addTeleportBtn = document.getElementById("addTeleportBtn");
|
|
if (addTeleportBtn) {
|
|
addTeleportBtn.addEventListener("click", function () {
|
|
setEditMode("teleport");
|
|
simulateClickOnSky();
|
|
});
|
|
} else {
|
|
console.warn("Add teleport button not found");
|
|
}
|
|
}
|
|
|
|
// Set edit mode
|
|
function setEditMode(mode) {
|
|
console.log("Setting edit mode to:", mode);
|
|
editorState.editMode = mode;
|
|
|
|
// Update UI
|
|
const toolButtons = document.querySelectorAll(".tool-btn");
|
|
toolButtons.forEach(btn => btn.classList.remove("active"));
|
|
|
|
const activeButton = document.getElementById(`${mode}Tool`);
|
|
if (activeButton) {
|
|
activeButton.classList.add("active");
|
|
}
|
|
|
|
// Update cursor based on mode
|
|
const scene = document.querySelector("a-scene");
|
|
if (scene) {
|
|
scene.style.cursor = mode === "select" ? "default" : "crosshair";
|
|
}
|
|
|
|
// Show mode in coordinate display
|
|
const coordinateDisplay = document.getElementById("coordinateDisplay");
|
|
if (coordinateDisplay) {
|
|
coordinateDisplay.textContent = `Mode: ${mode.charAt(0).toUpperCase() + mode.slice(1)}`;
|
|
}
|
|
}
|
|
|
|
// Handle scene loaded event
|
|
function handleSceneLoaded(event) {
|
|
console.log("Scene loaded in editor");
|
|
|
|
// Get the scene
|
|
const scene = document.querySelector("a-scene");
|
|
if (!scene) {
|
|
console.error("A-Scene element not found");
|
|
return;
|
|
}
|
|
|
|
// Get scene ID from the parent quackscape-scene element
|
|
const sceneElement = scene.closest("quackscape-scene");
|
|
if (!sceneElement) {
|
|
console.error("Parent quackscape-scene element not found");
|
|
return;
|
|
}
|
|
|
|
const sceneId = sceneElement.getAttribute("scene");
|
|
console.log("Scene ID:", sceneId);
|
|
|
|
if (!sceneId) {
|
|
console.error("Scene ID not found on quackscape-scene element");
|
|
return;
|
|
}
|
|
|
|
// Load scene data
|
|
getScene(sceneId).then(response => {
|
|
console.log("Scene data loaded:", response.obj);
|
|
editorState.scene = response.obj;
|
|
editorState.originalData = JSON.parse(JSON.stringify(response.obj));
|
|
|
|
// Update element count
|
|
updateElementCount();
|
|
|
|
// Populate elements list
|
|
populateElementsList();
|
|
|
|
// Fill scene settings form
|
|
fillSceneSettingsForm();
|
|
|
|
// Log scene ID for debugging
|
|
console.log("Scene ID stored in editorState:", editorState.scene.id);
|
|
}).catch(error => {
|
|
console.error("Error loading scene data:", error);
|
|
showNotification("Failed to load scene data", "error");
|
|
});
|
|
|
|
// Add event listeners to all elements
|
|
setupElementEventListeners(scene);
|
|
|
|
// Set up scene click handler
|
|
setupSceneClickHandler();
|
|
|
|
// Add click listener to sky for adding new elements
|
|
const sky = scene.querySelector("a-sky");
|
|
if (sky) {
|
|
sky.addEventListener("click", handleSkyClick);
|
|
console.log("Added click listener to sky");
|
|
} else {
|
|
console.error("Sky element not found");
|
|
}
|
|
}
|
|
|
|
// Fill scene settings form
|
|
function fillSceneSettingsForm() {
|
|
if (!editorState.scene) return;
|
|
|
|
const titleInput = document.getElementById("sceneTitle");
|
|
const descriptionInput = document.getElementById("sceneDescription");
|
|
const defaultXInput = document.getElementById("defaultX");
|
|
const defaultYInput = document.getElementById("defaultY");
|
|
const defaultZInput = document.getElementById("defaultZ");
|
|
const publicCheckbox = document.getElementById("scenePublic");
|
|
|
|
if (titleInput) titleInput.value = editorState.scene.title || "";
|
|
if (descriptionInput) descriptionInput.value = editorState.scene.description || "";
|
|
if (defaultXInput) defaultXInput.value = editorState.scene.default_x || 0;
|
|
if (defaultYInput) defaultYInput.value = editorState.scene.default_y || 0;
|
|
if (defaultZInput) defaultZInput.value = editorState.scene.default_z || 0;
|
|
if (publicCheckbox) publicCheckbox.checked = editorState.scene.public !== false;
|
|
}
|
|
|
|
// Set up element event listeners
|
|
function setupElementEventListeners(scene) {
|
|
// Get all interactive elements
|
|
const elements = scene.querySelectorAll("[id]");
|
|
|
|
elements.forEach(element => {
|
|
if (element.tagName.startsWith("A-") &&
|
|
element.tagName !== "A-SCENE" &&
|
|
element.tagName !== "A-ASSETS" &&
|
|
element.tagName !== "A-SKY" &&
|
|
element.tagName !== "A-CAMERA") {
|
|
|
|
console.log("Setting up event listeners for element:", element.tagName, element.getAttribute("id"));
|
|
|
|
// Make element clickable
|
|
element.setAttribute("data-clickable", "");
|
|
|
|
// Remove original onclick events
|
|
if (element.hasAttribute("onclick")) {
|
|
const onclickValue = element.getAttribute("onclick");
|
|
element.removeAttribute("onclick");
|
|
element.dataset.originalOnclick = onclickValue;
|
|
}
|
|
|
|
// Add event listeners
|
|
element.addEventListener("mousedown", handleElementMouseDown);
|
|
element.addEventListener("mouseup", handleElementMouseUp);
|
|
element.addEventListener("click", handleElementClick);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle mouse down on element
|
|
function handleElementMouseDown(event) {
|
|
editorState.clickTimestamp = event.timeStamp;
|
|
editorState.isDragging = false;
|
|
|
|
// Make element draggable in select mode
|
|
if (editorState.editMode === "select") {
|
|
event.target.setAttribute("click-drag", "enabled: true;");
|
|
}
|
|
}
|
|
|
|
// Handle mouse up on element
|
|
function handleElementMouseUp(event) {
|
|
const dragDuration = event.timeStamp - editorState.clickTimestamp;
|
|
|
|
// If it was a short click, treat as a click, not a drag
|
|
if (dragDuration < 200) {
|
|
editorState.isDragging = false;
|
|
} else {
|
|
editorState.isDragging = true;
|
|
updateElementLocation(event);
|
|
}
|
|
}
|
|
|
|
// Handle click on element
|
|
function handleElementClick(event) {
|
|
// Ignore if we're dragging
|
|
if (editorState.isDragging) return;
|
|
|
|
console.log("Element clicked:", event.target.tagName, event.target.getAttribute("id"));
|
|
|
|
// In select mode, edit the element
|
|
if (editorState.editMode === "select") {
|
|
startModifyElement(event);
|
|
}
|
|
}
|
|
|
|
// Simplified click handler for adding new elements
|
|
function setupSceneClickHandler() {
|
|
console.log("Setting up custom scene click handler");
|
|
|
|
const scene = document.querySelector("a-scene");
|
|
if (!scene) return;
|
|
|
|
// Add click handler to the scene
|
|
scene.addEventListener("click", function (event) {
|
|
// Only handle clicks in creation modes
|
|
if (editorState.editMode === "select") return;
|
|
|
|
// Check if we clicked on an element (not the background)
|
|
if (event.target.tagName !== "A-SCENE" &&
|
|
event.target.tagName !== "A-SKY" &&
|
|
event.target.tagName !== "CANVAS") {
|
|
console.log("Clicked on element, not background");
|
|
return;
|
|
}
|
|
|
|
console.log("Background clicked in creation mode:", editorState.editMode);
|
|
|
|
// Get the camera and raycaster
|
|
const camera = scene.camera;
|
|
if (!camera) {
|
|
console.warn("Camera not found");
|
|
return;
|
|
}
|
|
|
|
// Get mouse position
|
|
const canvas = scene.canvas;
|
|
const rect = canvas.getBoundingClientRect();
|
|
const mouseX = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
const mouseY = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
|
|
console.log("Normalized mouse coordinates:", mouseX, mouseY);
|
|
|
|
// Create raycaster
|
|
const raycaster = new THREE.Raycaster();
|
|
raycaster.setFromCamera(new THREE.Vector2(mouseX, mouseY), camera);
|
|
|
|
// Calculate intersection with the sky sphere
|
|
// Create a sphere geometry for intersection testing
|
|
const sphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 5);
|
|
|
|
// Calculate ray intersection with sphere
|
|
const ray = raycaster.ray;
|
|
const intersection = new THREE.Vector3();
|
|
ray.intersectSphere(sphere, intersection);
|
|
|
|
console.log("Ray intersection with sphere:", intersection);
|
|
|
|
// Create synthetic event with the calculated position
|
|
const syntheticEvent = {
|
|
detail: {
|
|
intersection: {
|
|
point: {
|
|
x: intersection.x,
|
|
y: intersection.y,
|
|
z: intersection.z
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Start creating element
|
|
startCreateElement(syntheticEvent);
|
|
});
|
|
|
|
console.log("Custom scene click handler set up");
|
|
}
|
|
|
|
|
|
// Create a preview element
|
|
function createElementPreview(elementType, position, properties) {
|
|
// Remove any existing preview
|
|
const existingPreview = document.querySelector(".element-preview");
|
|
if (existingPreview) {
|
|
existingPreview.parentNode.removeChild(existingPreview);
|
|
}
|
|
|
|
// Constrain the initial position to a sphere
|
|
const radius = (elementType === "MarkerElement") ? 5 : 5;
|
|
const constrained = constrainToSphere(position.x, position.y, position.z, radius);
|
|
console.log("Initial position:", position, "Constrained:", constrained);
|
|
|
|
// Create element based on type
|
|
let previewElement;
|
|
|
|
switch (elementType) {
|
|
case "MarkerElement":
|
|
case "TeleportElement":
|
|
previewElement = document.createElement("a-entity");
|
|
previewElement.setAttribute("geometry", "primitive: sphere; radius: 0.2");
|
|
previewElement.setAttribute("material", "color: #4F46E5; shader: flat");
|
|
|
|
// Make the marker look at the center (invert the normal direction)
|
|
previewElement.setAttribute("look-at", "0 0 0");
|
|
|
|
// Create a separate text entity that will always face the camera
|
|
const textEntity = document.createElement("a-text");
|
|
textEntity.setAttribute("value", properties.title || 'New Marker');
|
|
textEntity.setAttribute("align", "center");
|
|
textEntity.setAttribute("width", 3);
|
|
textEntity.setAttribute("color", "white");
|
|
textEntity.setAttribute("position", "0 0.3 0");
|
|
|
|
// Make the text always face the camera
|
|
textEntity.setAttribute("billboard", "");
|
|
|
|
// Changes for TeleportElement
|
|
if (elementType === "TeleportElement") {
|
|
previewElement.setAttribute("material", "color: #10B981; shader: flat");
|
|
textEntity.setAttribute("value", properties.title || 'New Teleport');
|
|
}
|
|
|
|
// Add the text as a child of the marker
|
|
previewElement.appendChild(textEntity);
|
|
break;
|
|
|
|
case "ImageElement":
|
|
previewElement = document.createElement("a-curvedimage");
|
|
previewElement.setAttribute("height", properties.height || 1);
|
|
previewElement.setAttribute("radius", radius);
|
|
previewElement.setAttribute("theta-length", 30);
|
|
|
|
// If we have an image preview, use it
|
|
const imagePreview = document.getElementById("imagePreview");
|
|
if (imagePreview && imagePreview.querySelector("img")) {
|
|
const imgSrc = imagePreview.querySelector("img").src;
|
|
previewElement.setAttribute("src", imgSrc);
|
|
} else {
|
|
// Use placeholder color
|
|
previewElement.setAttribute("material", elementType === "TeleportElement" ?
|
|
"color: #10B981" : "color: #3B82F6");
|
|
}
|
|
|
|
// Calculate theta-start based on position
|
|
const thetaStart = Math.atan2(constrained.z, constrained.x) * (180 / Math.PI);
|
|
previewElement.setAttribute("theta-start", ((thetaStart + 270) % 360));
|
|
break;
|
|
|
|
default:
|
|
console.error("Unknown element type for preview:", elementType);
|
|
return;
|
|
}
|
|
|
|
// Set common attributes
|
|
previewElement.classList.add("element-preview");
|
|
previewElement.setAttribute("data-clickable", "");
|
|
previewElement.setAttribute("position", `${constrained.x} ${constrained.y} ${constrained.z}`);
|
|
previewElement.setAttribute("click-drag", "");
|
|
|
|
// Add to scene
|
|
const scene = document.querySelector("a-scene");
|
|
if (scene) {
|
|
scene.appendChild(previewElement);
|
|
|
|
// Add custom drag handler
|
|
previewElement.addEventListener("mousedown", startDrag);
|
|
document.addEventListener("mousemove", handleDrag);
|
|
document.addEventListener("mouseup", endDrag);
|
|
|
|
return previewElement;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Function to handle drag end
|
|
function handlePreviewDragEnd(event) {
|
|
console.log("Preview element dragged", event);
|
|
|
|
// Get the new position from the element
|
|
const element = event.target;
|
|
const position = element.getAttribute("position");
|
|
|
|
if (!position) {
|
|
console.warn("No position attribute after drag");
|
|
return;
|
|
}
|
|
|
|
console.log("Raw position after drag:", position);
|
|
|
|
// Constrain the position to a sphere
|
|
const elementType = document.getElementById("resourcetype")?.value;
|
|
const radius = (elementType === "MarkerElement") ? 5 : 5;
|
|
|
|
const constrained = constrainToSphere(position.x, position.y, position.z, radius);
|
|
console.log("Constrained position:", constrained);
|
|
|
|
// Update the element position to stay on the sphere
|
|
element.setAttribute("position", `${constrained.x} ${constrained.y} ${constrained.z}`);
|
|
|
|
// Update form values
|
|
const posXInput = document.getElementById("position_x");
|
|
const posYInput = document.getElementById("position_y");
|
|
const posZInput = document.getElementById("position_z");
|
|
|
|
if (posXInput) posXInput.value = constrained.x.toFixed(2);
|
|
if (posYInput) posYInput.value = constrained.y.toFixed(2);
|
|
if (posZInput) posZInput.value = constrained.z.toFixed(2);
|
|
}
|
|
|
|
// Start creating a new element
|
|
function startCreateElement(event) {
|
|
console.log("Starting element creation", event);
|
|
|
|
const propertiesTitle = document.getElementById("propertiesTitle");
|
|
if (propertiesTitle) {
|
|
propertiesTitle.textContent = "Create Element";
|
|
}
|
|
|
|
const propertiesContent = document.getElementById("propertiesContent");
|
|
if (!propertiesContent) {
|
|
console.error("Properties content element not found");
|
|
return;
|
|
}
|
|
|
|
// Calculate position from intersection point
|
|
let position = { x: 0, y: 1.6, z: -5 }; // Default position
|
|
|
|
try {
|
|
if (event.detail && event.detail.intersection && event.detail.intersection.point) {
|
|
position = {
|
|
x: parseFloat(event.detail.intersection.point.x) || 0,
|
|
y: parseFloat(event.detail.intersection.point.y) || 1.6,
|
|
z: parseFloat(event.detail.intersection.point.z) || -5
|
|
};
|
|
}
|
|
} catch (e) {
|
|
console.warn("Error extracting position from event:", e);
|
|
}
|
|
|
|
console.log("Using position for new element:", position);
|
|
|
|
// Create form based on element type
|
|
let elementType;
|
|
switch (editorState.editMode) {
|
|
case "marker":
|
|
elementType = "MarkerElement";
|
|
break;
|
|
case "image":
|
|
elementType = "ImageElement";
|
|
break;
|
|
case "teleport":
|
|
elementType = "TeleportElement";
|
|
break;
|
|
default:
|
|
console.error("Unknown edit mode:", editorState.editMode);
|
|
return;
|
|
}
|
|
|
|
console.log("Creating element of type:", elementType);
|
|
|
|
// Create debug info
|
|
const debugInfo = `
|
|
<div class="hide" id="propertiesDebug">
|
|
<b>Creating ${elementType} at:</b><br/>
|
|
X: ${position.x.toFixed(2)}<br/>
|
|
Y: ${position.y.toFixed(2)}<br/>
|
|
Z: ${position.z.toFixed(2)}<br/>
|
|
<hr/>
|
|
</div>
|
|
`;
|
|
|
|
// Create form
|
|
propertiesContent.innerHTML = `
|
|
${debugInfo}
|
|
<form id="newElementForm">
|
|
<input type="hidden" id="resourcetype" value="${elementType}">
|
|
<input type="hidden" id="csrfmiddlewaretoken" value="${getCookie("csrftoken")}">
|
|
<div id="elementProperties"></div>
|
|
</form>
|
|
`;
|
|
|
|
// Create element properties form
|
|
createElementPropertiesForm(elementType, position);
|
|
|
|
// Create preview element with the exact same position
|
|
createElementPreview(elementType, position, { title: "" });
|
|
|
|
// Show buttons
|
|
const resetButton = document.getElementById("resetButton");
|
|
if (resetButton) {
|
|
resetButton.classList.remove("hide");
|
|
}
|
|
|
|
const buttons = document.getElementById("buttons");
|
|
if (buttons) {
|
|
buttons.classList.remove("hide");
|
|
}
|
|
|
|
// Set up save button
|
|
const saveButton = document.getElementById("saveButton");
|
|
if (saveButton) {
|
|
saveButton.onclick = saveNewElement;
|
|
}
|
|
|
|
// Set up cancel button
|
|
const cancelButton = document.getElementById("cancelButton");
|
|
if (cancelButton) {
|
|
cancelButton.onclick = cancelEdit;
|
|
}
|
|
|
|
// Show properties tab
|
|
const propertiesTab = document.getElementById("properties-tab");
|
|
if (propertiesTab) {
|
|
propertiesTab.click();
|
|
}
|
|
}
|
|
|
|
// Position listeners to update the preview element
|
|
function addPositionInputListeners() {
|
|
const posXInput = document.getElementById("position_x");
|
|
const posYInput = document.getElementById("position_y");
|
|
const posZInput = document.getElementById("position_z");
|
|
|
|
const updatePreviewPosition = () => {
|
|
const previewElement = document.querySelector(".element-preview");
|
|
if (!previewElement) return;
|
|
|
|
const x = parseFloat(posXInput.value) || 0;
|
|
const y = parseFloat(posYInput.value) || 0;
|
|
const z = parseFloat(posZInput.value) || 0;
|
|
|
|
// Update the stored position
|
|
editorState.currentPosition = { x, y, z };
|
|
|
|
// Update the preview element position
|
|
previewElement.setAttribute("position", `${x} ${y} ${z}`);
|
|
|
|
console.log("Updated position from form inputs:", editorState.currentPosition);
|
|
};
|
|
|
|
if (posXInput) posXInput.addEventListener("change", updatePreviewPosition);
|
|
if (posYInput) posYInput.addEventListener("change", updatePreviewPosition);
|
|
if (posZInput) posZInput.addEventListener("change", updatePreviewPosition);
|
|
}
|
|
|
|
function createElementPropertiesForm(elementType, position = { x: 0, y: 0, z: -5 }) {
|
|
const elementProperties = document.getElementById("elementProperties");
|
|
if (!elementProperties) {
|
|
console.error("Element properties container not found");
|
|
return;
|
|
}
|
|
|
|
// Common fields for all element types
|
|
let formHTML = `
|
|
<div class="form-group">
|
|
<label for="title" class="form-label">Title</label>
|
|
<input type="text" class="form-control" id="title" placeholder="Title" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="position" class="form-label">Position</label>
|
|
<div class="input-group mb-2">
|
|
<span class="input-group-text">X</span>
|
|
<input type="number" class="form-control" id="position_x" name="position_x" value="${position.x.toFixed(2)}" step="0.1">
|
|
</div>
|
|
<div class="input-group mb-2">
|
|
<span class="input-group-text">Y</span>
|
|
<input type="number" class="form-control" id="position_y" name="position_y" value="${position.y.toFixed(2)}" step="0.1">
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-text">Z</span>
|
|
<input type="number" class="form-control" id="position_z" name="position_z" value="${position.z.toFixed(2)}" step="0.1">
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add element-specific fields
|
|
if (elementType === "ImageElement") {
|
|
formHTML += `
|
|
<div class="form-group">
|
|
<label for="elementUpload" class="form-label">Image</label>
|
|
<input id="elementUpload" type="file" class="form-control" accept="image/*" required>
|
|
<div class="form-text">Select an image to display</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="elementSize" class="form-label">Size</label>
|
|
<div class="input-group mb-2">
|
|
<span class="input-group-text">Width</span>
|
|
<input type="number" class="form-control" id="width" name="width" value="2" min="0.1" step="0.1">
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-text">Height</span>
|
|
<input type="number" class="form-control" id="height" name="height" value="1" min="0.1" step="0.1">
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Add teleport-specific fields
|
|
if (elementType === "TeleportElement") {
|
|
formHTML += `
|
|
<div class="form-group">
|
|
<label for="destinationDropdownSearch" class="form-label">Teleport Destination</label>
|
|
<div class="dropdown-container" style="position: relative;">
|
|
<input class="form-control" autocomplete="off" id="destinationDropdownSearch" type="text" placeholder="Search for a scene..." required>
|
|
<input type="hidden" id="destination" name="destination" required>
|
|
<div id="destinationDropdownMenu" style="width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Destination View</label>
|
|
<div class="input-group mb-2">
|
|
<span class="input-group-text">X</span>
|
|
<input type="number" class="form-control" id="destination_x" name="destination_x" value="0" step="1">
|
|
</div>
|
|
<div class="input-group mb-2">
|
|
<span class="input-group-text">Y</span>
|
|
<input type="number" class="form-control" id="destination_y" name="destination_y" value="0" step="1">
|
|
</div>
|
|
<div class="input-group">
|
|
<span class="input-group-text">Z</span>
|
|
<input type="number" class="form-control" id="destination_z" name="destination_z" value="0" step="1">
|
|
</div>
|
|
<div class="form-text">Set the initial view when teleporting to the destination</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
elementProperties.innerHTML = formHTML;
|
|
|
|
// Add position input listeners
|
|
addPositionInputListeners();
|
|
|
|
// Set up destination dropdown for teleport elements
|
|
if (elementType === "TeleportElement" && editorState.scene) {
|
|
const categoryId = editorState.scene.category;
|
|
|
|
getCategory(categoryId).then(category => {
|
|
populateDestinationDropdown(null, category);
|
|
}).catch(error => {
|
|
console.error("Error loading category data for destination dropdown:", error);
|
|
showNotification("Error loading scenes for destination selection", "error");
|
|
});
|
|
}
|
|
|
|
// Set up image preview for image upload
|
|
const elementUpload = document.getElementById("elementUpload");
|
|
if (elementUpload) {
|
|
elementUpload.addEventListener("change", function () {
|
|
if (this.files && this.files[0]) {
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function (e) {
|
|
// Create or update preview
|
|
let preview = document.getElementById("imagePreview");
|
|
if (!preview) {
|
|
preview = document.createElement("div");
|
|
preview.id = "imagePreview";
|
|
preview.className = "image-preview mt-2";
|
|
elementUpload.parentNode.appendChild(preview);
|
|
}
|
|
|
|
preview.innerHTML = `<img src="${e.target.result}" alt="Preview" class="img-fluid">`;
|
|
|
|
// Update the preview element with the new image
|
|
const previewElement = document.querySelector(".element-preview");
|
|
if (previewElement && previewElement.tagName === "A-CURVEDIMAGE") {
|
|
previewElement.setAttribute("src", e.target.result);
|
|
}
|
|
};
|
|
|
|
reader.readAsDataURL(this.files[0]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Start modifying an existing element
|
|
function startModifyElement(event) {
|
|
console.log("Starting element modification");
|
|
|
|
// Make the element draggable for positioning
|
|
event.target.setAttribute("click-drag", "enabled: true;");
|
|
|
|
// Listen for "dragend" event to update the element's position
|
|
event.target.addEventListener("dragend", updateElementLocation);
|
|
|
|
// Set active element
|
|
editorState.activeElement = event.target;
|
|
|
|
// editor.js (continued)
|
|
// Update UI
|
|
const propertiesTitle = document.getElementById("propertiesTitle");
|
|
if (propertiesTitle) {
|
|
propertiesTitle.textContent = "Edit Element";
|
|
}
|
|
|
|
// Get element data from API
|
|
const scene = findParentScene(event.target);
|
|
if (!scene) {
|
|
console.error("Parent scene not found");
|
|
return;
|
|
}
|
|
|
|
const elementId = event.target.getAttribute("id");
|
|
if (!elementId) {
|
|
console.error("Element ID not found");
|
|
return;
|
|
}
|
|
|
|
getSceneElement(scene.getAttribute("id"), elementId).then(element_data => {
|
|
console.log("Element data loaded:", element_data.obj);
|
|
|
|
const propertiesContent = document.getElementById("propertiesContent");
|
|
if (!propertiesContent) {
|
|
console.error("Properties content element not found");
|
|
return;
|
|
}
|
|
|
|
// Create debug info
|
|
const debugHTML = `
|
|
<div class="hide" id="propertiesDebug">
|
|
<b>Modifying element:</b><br/>
|
|
Element Type: ${event.target.tagName}<br/>
|
|
Element ID: ${elementId}<br/>
|
|
Element data: ${JSON.stringify(element_data.obj)}<br/>
|
|
<hr/>
|
|
</div>
|
|
`;
|
|
|
|
// Create form
|
|
propertiesContent.innerHTML = `
|
|
${debugHTML}
|
|
<form id="modifyElementForm">
|
|
<input type="hidden" id="csrfmiddlewaretoken" value="${getCookie("csrftoken")}">
|
|
<input type="hidden" id="id" value="${elementId}">
|
|
<div id="elementProperties"></div>
|
|
</form>
|
|
`;
|
|
|
|
// Create element properties form
|
|
createElementPropertiesForm(element_data.obj.resourcetype);
|
|
|
|
// Fill form with element data
|
|
fillFormWithElementData(element_data.obj);
|
|
|
|
// Show buttons
|
|
document.getElementById("resetButton")?.classList.remove("hide");
|
|
document.getElementById("deleteButton")?.classList.remove("hide");
|
|
document.getElementById("buttons")?.classList.remove("hide");
|
|
|
|
// Set up save button
|
|
const saveButton = document.getElementById("saveButton");
|
|
if (saveButton) {
|
|
saveButton.onclick = saveModifiedElement;
|
|
}
|
|
|
|
// Set up delete button
|
|
const deleteButton = document.getElementById("deleteButton");
|
|
if (deleteButton) {
|
|
deleteButton.onclick = deleteElement;
|
|
}
|
|
|
|
// Set up cancel button
|
|
const cancelButton = document.getElementById("cancelButton");
|
|
if (cancelButton) {
|
|
cancelButton.onclick = cancelEdit;
|
|
}
|
|
|
|
// Show properties tab
|
|
const propertiesTab = document.getElementById("properties-tab");
|
|
if (propertiesTab) {
|
|
propertiesTab.click();
|
|
}
|
|
}).catch(error => {
|
|
console.error("Error loading element data:", error);
|
|
showNotification("Failed to load element data", "error");
|
|
});
|
|
}
|
|
|
|
// Fill form with element data
|
|
function fillFormWithElementData(elementData) {
|
|
console.log("Filling form with element data:", elementData);
|
|
|
|
// Fill common fields
|
|
const titleInput = document.getElementById("title");
|
|
const positionXInput = document.getElementById("position_x");
|
|
const positionYInput = document.getElementById("position_y");
|
|
const positionZInput = document.getElementById("position_z");
|
|
|
|
if (titleInput) titleInput.value = elementData.label || "";
|
|
if (positionXInput) positionXInput.value = elementData.x || 0;
|
|
if (positionYInput) positionYInput.value = elementData.y || 0;
|
|
if (positionZInput) positionZInput.value = elementData.z || 5;
|
|
|
|
// Fill element-specific fields
|
|
if (elementData.resourcetype === "ImageElement" || elementData.resourcetype === "TeleportElement") {
|
|
const widthInput = document.getElementById("width");
|
|
const heightInput = document.getElementById("height");
|
|
|
|
if (widthInput) widthInput.value = elementData.width || 2;
|
|
if (heightInput) heightInput.value = elementData.height || 1;
|
|
|
|
// Show current image
|
|
if (elementData.image) {
|
|
const imagePreview = document.createElement("div");
|
|
imagePreview.id = "imagePreview";
|
|
imagePreview.className = "image-preview mt-2";
|
|
imagePreview.innerHTML = `
|
|
<div class="current-image-label">Current Image:</div>
|
|
<img src="${elementData.image}" alt="Current Image" class="img-fluid">
|
|
`;
|
|
|
|
const elementUpload = document.getElementById("elementUpload");
|
|
if (elementUpload) {
|
|
elementUpload.parentNode.appendChild(imagePreview);
|
|
// Make image upload optional for editing
|
|
elementUpload.removeAttribute("required");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fill teleport-specific fields
|
|
if (elementData.resourcetype === "TeleportElement") {
|
|
const destinationInput = document.getElementById("destination");
|
|
const destinationXInput = document.getElementById("destination_x");
|
|
const destinationYInput = document.getElementById("destination_y");
|
|
const destinationZInput = document.getElementById("destination_z");
|
|
|
|
if (destinationInput) destinationInput.value = elementData.destination || "";
|
|
|
|
// Set destination view if not default (-1)
|
|
if (elementData.destination_x !== -1 && destinationXInput) {
|
|
destinationXInput.value = elementData.destination_x;
|
|
}
|
|
if (elementData.destination_y !== -1 && destinationYInput) {
|
|
destinationYInput.value = elementData.destination_y;
|
|
}
|
|
if (elementData.destination_z !== -1 && destinationZInput) {
|
|
destinationZInput.value = elementData.destination_z;
|
|
}
|
|
|
|
// Get scene info to populate destination dropdown
|
|
getScene(elementData.scene).then(scene_data => {
|
|
getCategory(scene_data.obj.category).then(category_data => {
|
|
populateDestinationDropdown(elementData.destination, category_data);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
function saveNewElement() {
|
|
console.log("Saving new element");
|
|
|
|
const form = document.getElementById("newElementForm");
|
|
if (!form) {
|
|
console.error("New element form not found");
|
|
return;
|
|
}
|
|
|
|
// Validate form
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
// Check if we have a valid scene ID
|
|
if (!editorState.scene || !editorState.scene.id) {
|
|
console.error("No valid scene ID found in editorState", editorState);
|
|
showNotification("Error: Scene ID not found", "error");
|
|
return;
|
|
}
|
|
|
|
// Get form data
|
|
const formData = new FormData(form);
|
|
const elementType = document.getElementById("resourcetype")?.value;
|
|
if (!elementType) {
|
|
console.error("Resource type not found");
|
|
showNotification("Error: Resource type not found", "error");
|
|
return;
|
|
}
|
|
|
|
formData.append("resourcetype", elementType);
|
|
|
|
// Get position values from form
|
|
const posXEl = document.getElementById("position_x");
|
|
const posYEl = document.getElementById("position_y");
|
|
const posZEl = document.getElementById("position_z");
|
|
|
|
const posX = posXEl ? parseFloat(posXEl.value) || 0 : 0;
|
|
const posY = posYEl ? parseFloat(posYEl.value) || 0 : 0;
|
|
const posZ = posZEl ? parseFloat(posZEl.value) || 0 : 0;
|
|
|
|
// Add common fields
|
|
const titleEl = document.getElementById("title");
|
|
formData.append("label", titleEl ? titleEl.value : "Unnamed Element");
|
|
formData.append("x", posX);
|
|
formData.append("y", posY);
|
|
formData.append("z", posZ);
|
|
formData.append("scene", editorState.scene.id);
|
|
|
|
console.log("Saving element at position:", { x: posX, y: posY, z: posZ });
|
|
console.log("Scene ID:", editorState.scene.id);
|
|
|
|
// Debug - log all form data
|
|
console.log("Form data before sending:");
|
|
for (const pair of formData.entries()) {
|
|
console.log(pair[0] + ": " + pair[1]);
|
|
}
|
|
|
|
// Add element-specific fields
|
|
if (elementType === "TeleportElement") {
|
|
const destinationEl = document.getElementById("destination");
|
|
const destXEl = document.getElementById("destination_x");
|
|
const destYEl = document.getElementById("destination_y");
|
|
const destZEl = document.getElementById("destination_z");
|
|
|
|
if (destinationEl) {
|
|
formData.append("destination", destinationEl.value);
|
|
} else {
|
|
console.error("Destination field not found");
|
|
showNotification("Error: Destination field not found", "error");
|
|
return;
|
|
}
|
|
|
|
formData.append("destination_x", destXEl ? destXEl.value : 0);
|
|
formData.append("destination_y", destYEl ? destYEl.value : 0);
|
|
formData.append("destination_z", destZEl ? destZEl.value : 0);
|
|
} else if (elementType === "ImageElement") {
|
|
const widthEl = document.getElementById("width");
|
|
const heightEl = document.getElementById("height");
|
|
|
|
formData.append("width", widthEl ? widthEl.value : 2);
|
|
formData.append("height", heightEl ? heightEl.value : 1);
|
|
|
|
// Add image file
|
|
const fileInput = document.getElementById("elementUpload");
|
|
if (fileInput && fileInput.files.length > 0) {
|
|
formData.append("image", fileInput.files[0]);
|
|
} else {
|
|
console.error("No image file selected");
|
|
showNotification("Error: Please select an image file", "error");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Show loading state
|
|
const saveButton = document.getElementById("saveButton");
|
|
if (saveButton) {
|
|
saveButton.disabled = true;
|
|
saveButton.innerHTML = '<i class="ph-light ph-spinner ph-spin"></i> Saving...';
|
|
}
|
|
|
|
// Log the form data for debugging
|
|
console.log("Form data entries:");
|
|
for (const pair of formData.entries()) {
|
|
console.log(pair[0] + ": " + pair[1]);
|
|
}
|
|
|
|
// Send API request
|
|
fetch(`/tours/api/scene/${editorState.scene.id}/elements/`, {
|
|
method: "POST",
|
|
body: formData,
|
|
headers: {
|
|
"X-CSRFToken": getCookie("csrftoken")
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(data => {
|
|
throw new Error(JSON.stringify(data));
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Success
|
|
console.log("Element created successfully:", data);
|
|
showNotification("Element created successfully", "success");
|
|
|
|
// Update editor state
|
|
editorState.modified = true;
|
|
|
|
// Reset form
|
|
cancelEdit();
|
|
|
|
// Reload scene to show new element
|
|
reloadScene();
|
|
})
|
|
.catch(error => {
|
|
console.error("Error creating element:", error);
|
|
let errorMessage = "Failed to create element";
|
|
|
|
try {
|
|
const errorData = JSON.parse(error.message);
|
|
if (typeof errorData === 'object') {
|
|
errorMessage = Object.entries(errorData)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join(', ');
|
|
}
|
|
} catch (e) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
showNotification(errorMessage, "error");
|
|
})
|
|
.finally(() => {
|
|
// Reset button state
|
|
if (saveButton) {
|
|
saveButton.disabled = false;
|
|
saveButton.innerHTML = '<i class="ph-light ph-check"></i> Save';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Save a modified element
|
|
function saveModifiedElement() {
|
|
console.log("Saving modified element");
|
|
|
|
if (!editorState.activeElement) {
|
|
console.error("No active element to save");
|
|
return;
|
|
}
|
|
|
|
const form = document.getElementById("modifyElementForm");
|
|
if (!form) {
|
|
console.error("Modify element form not found");
|
|
return;
|
|
}
|
|
|
|
// Validate form
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
// Get form data
|
|
const formData = new FormData(form);
|
|
const elementId = document.getElementById("id").value;
|
|
|
|
// Add common fields
|
|
formData.append("label", document.getElementById("title").value);
|
|
formData.append("x", document.getElementById("position_x").value);
|
|
formData.append("y", document.getElementById("position_y").value);
|
|
formData.append("z", document.getElementById("position_z").value);
|
|
formData.append("scene", editorState.scene.id);
|
|
|
|
// Add element-specific fields
|
|
const elementType = editorState.activeElement.tagName.toLowerCase();
|
|
if (elementType === "a-curvedimage") {
|
|
const widthInput = document.getElementById("width");
|
|
const heightInput = document.getElementById("height");
|
|
|
|
if (widthInput) formData.append("width", widthInput.value);
|
|
if (heightInput) formData.append("height", heightInput.value);
|
|
|
|
// Add image file if provided
|
|
const fileInput = document.getElementById("elementUpload");
|
|
if (fileInput && fileInput.files.length > 0) {
|
|
formData.append("image", fileInput.files[0]);
|
|
}
|
|
}
|
|
|
|
// Add teleport-specific fields
|
|
if (elementType === "a-entity" && editorState.activeElement.hasAttribute("onclick")) {
|
|
const destinationInput = document.getElementById("destination");
|
|
const destinationXInput = document.getElementById("destination_x");
|
|
const destinationYInput = document.getElementById("destination_y");
|
|
const destinationZInput = document.getElementById("destination_z");
|
|
|
|
if (destinationInput) formData.append("destination", destinationInput.value);
|
|
if (destinationXInput) formData.append("destination_x", destinationXInput.value);
|
|
if (destinationYInput) formData.append("destination_y", destinationYInput.value);
|
|
if (destinationZInput) formData.append("destination_z", destinationZInput.value);
|
|
}
|
|
|
|
// Show loading state
|
|
const saveButton = document.getElementById("saveButton");
|
|
if (saveButton) {
|
|
saveButton.disabled = true;
|
|
saveButton.innerHTML = '<i class="ph-light ph-spinner ph-spin"></i> Saving...';
|
|
}
|
|
|
|
// Send API request
|
|
fetch(`/tours/api/scene/${editorState.scene.id}/elements/${elementId}/`, {
|
|
method: "PUT",
|
|
body: formData,
|
|
headers: {
|
|
"X-CSRFToken": getCookie("csrftoken")
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(data => {
|
|
throw new Error(JSON.stringify(data));
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Success
|
|
console.log("Element updated successfully:", data);
|
|
showNotification("Element updated successfully", "success");
|
|
|
|
// Update editor state
|
|
editorState.modified = true;
|
|
|
|
// Reset form
|
|
cancelEdit();
|
|
|
|
// Reload scene to show updated element
|
|
reloadScene();
|
|
})
|
|
.catch(error => {
|
|
console.error("Error updating element:", error);
|
|
let errorMessage = "Failed to update element";
|
|
|
|
try {
|
|
const errorData = JSON.parse(error.message);
|
|
if (typeof errorData === 'object') {
|
|
errorMessage = Object.entries(errorData)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join(', ');
|
|
}
|
|
} catch (e) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
showNotification(errorMessage, "error");
|
|
})
|
|
.finally(() => {
|
|
// Reset button state
|
|
if (saveButton) {
|
|
saveButton.disabled = false;
|
|
saveButton.innerHTML = '<i class="ph-light ph-check"></i> Save';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Delete an element
|
|
function deleteElement() {
|
|
console.log("Deleting element");
|
|
|
|
if (!editorState.activeElement) {
|
|
console.error("No active element to delete");
|
|
return;
|
|
}
|
|
|
|
const elementId = editorState.activeElement.getAttribute("id");
|
|
if (!elementId) {
|
|
console.error("Element ID not found");
|
|
return;
|
|
}
|
|
|
|
// Confirm deletion
|
|
if (!confirm("Are you sure you want to delete this element?")) {
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
const deleteButton = document.getElementById("deleteButton");
|
|
if (deleteButton) {
|
|
deleteButton.disabled = true;
|
|
deleteButton.innerHTML = '<i class="ph-light ph-spinner ph-spin"></i> Deleting...';
|
|
}
|
|
|
|
// Send API request
|
|
fetch(`/tours/api/scene/${editorState.scene.id}/elements/${elementId}/`, {
|
|
method: "DELETE",
|
|
headers: {
|
|
"X-CSRFToken": getCookie("csrftoken")
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
}
|
|
|
|
// Success
|
|
console.log("Element deleted successfully");
|
|
showNotification("Element deleted successfully", "success");
|
|
|
|
// Update editor state
|
|
editorState.modified = true;
|
|
|
|
// Reset form
|
|
cancelEdit();
|
|
|
|
// Reload scene to reflect deletion
|
|
reloadScene();
|
|
})
|
|
.catch(error => {
|
|
console.error("Error deleting element:", error);
|
|
showNotification("Failed to delete element: " + error.message, "error");
|
|
})
|
|
.finally(() => {
|
|
// Reset button state
|
|
if (deleteButton) {
|
|
deleteButton.disabled = false;
|
|
deleteButton.innerHTML = '<i class="ph-light ph-trash"></i> Delete';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Simulates a click on the sky to start creating an element
|
|
function simulateClickOnSky() {
|
|
console.log("Simulating click on sky");
|
|
|
|
// Get camera position and direction
|
|
const camera = document.querySelector("a-camera");
|
|
if (!camera || !camera.object3D) {
|
|
console.warn("Camera not found for simulated click");
|
|
|
|
// Use default position
|
|
const syntheticEvent = {
|
|
detail: {
|
|
intersection: {
|
|
point: { x: 0, y: 0, z: -5 }
|
|
}
|
|
}
|
|
};
|
|
|
|
startCreateElement(syntheticEvent);
|
|
return;
|
|
}
|
|
|
|
// Get camera direction
|
|
const direction = new THREE.Vector3(0, 0, -1);
|
|
direction.applyQuaternion(camera.object3D.quaternion);
|
|
direction.normalize();
|
|
|
|
// Position at radius 5 in the direction the camera is facing
|
|
const position = direction.multiplyScalar(5);
|
|
|
|
// Create synthetic event
|
|
const syntheticEvent = {
|
|
detail: {
|
|
intersection: {
|
|
point: {
|
|
x: position.x,
|
|
y: position.y,
|
|
z: position.z
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Call the handler directly
|
|
startCreateElement(syntheticEvent);
|
|
}
|
|
|
|
function startDrag(event) {
|
|
const element = event.target.closest(".element-preview");
|
|
if (!element) return;
|
|
|
|
dragState.isDragging = true;
|
|
dragState.element = element;
|
|
dragState.lastMouseX = event.clientX;
|
|
dragState.lastMouseY = event.clientY;
|
|
|
|
// Get the element type to determine radius
|
|
const elementType = document.getElementById("resourcetype")?.value;
|
|
dragState.radius = (elementType === "MarkerElement") ? 5 : 5;
|
|
|
|
console.log("Started dragging element", element.tagName);
|
|
|
|
// Prevent default to avoid text selection
|
|
event.preventDefault();
|
|
}
|
|
|
|
function handleDrag(event) {
|
|
if (!dragState.isDragging || !dragState.element) return;
|
|
|
|
// Get the scene and camera
|
|
const scene = document.querySelector("a-scene");
|
|
if (!scene || !scene.camera) return;
|
|
|
|
// Calculate mouse movement
|
|
const deltaX = event.clientX - dragState.lastMouseX;
|
|
const deltaY = event.clientY - dragState.lastMouseY;
|
|
dragState.lastMouseX = event.clientX;
|
|
dragState.lastMouseY = event.clientY;
|
|
|
|
// Get current position
|
|
const position = dragState.element.getAttribute("position");
|
|
if (!position) return;
|
|
|
|
// Convert to spherical coordinates
|
|
const spherical = cartesianToSpherical(position.x, position.y, position.z);
|
|
|
|
// Update spherical coordinates based on mouse movement
|
|
// Scale factors determine how fast the element moves with mouse movement
|
|
const thetaScale = 0.01;
|
|
const phiScale = 0.01;
|
|
|
|
// Horizontal movement changes theta (rotation around y-axis)
|
|
spherical.theta -= deltaX * thetaScale;
|
|
|
|
// Vertical movement changes phi (angle from y-axis)
|
|
spherical.phi += deltaY * phiScale;
|
|
|
|
// Constrain phi to avoid flipping at poles
|
|
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
|
|
|
|
// Convert back to cartesian
|
|
const newPos = sphericalToCartesian(spherical.radius, spherical.phi, spherical.theta);
|
|
|
|
// Update element position
|
|
dragState.element.setAttribute("position", `${newPos.x} ${newPos.y} ${newPos.z}`);
|
|
|
|
// If it's a marker, make it face the center
|
|
if (dragState.element.hasAttribute("look-at")) {
|
|
dragState.element.setAttribute("look-at", "0 0 0");
|
|
}
|
|
|
|
// If it's a curved image, update theta-start
|
|
if (dragState.element.tagName === "A-CURVEDIMAGE") {
|
|
const thetaStart = Math.atan2(newPos.z, newPos.x) * (180 / Math.PI);
|
|
dragState.element.setAttribute("theta-start", ((thetaStart + 270) % 360));
|
|
}
|
|
|
|
// Update form values
|
|
const posXInput = document.getElementById("position_x");
|
|
const posYInput = document.getElementById("position_y");
|
|
const posZInput = document.getElementById("position_z");
|
|
|
|
if (posXInput) posXInput.value = newPos.x.toFixed(2);
|
|
if (posYInput) posYInput.value = newPos.y.toFixed(2);
|
|
if (posZInput) posZInput.value = newPos.z.toFixed(2);
|
|
}
|
|
|
|
function endDrag(event) {
|
|
if (!dragState.isDragging) return;
|
|
|
|
console.log("Ended dragging");
|
|
|
|
// Trigger dragend event on the element
|
|
if (dragState.element) {
|
|
const element = dragState.element;
|
|
const position = element.getAttribute("position");
|
|
|
|
// Create and dispatch a custom event
|
|
const dragEndEvent = new CustomEvent("dragend", {
|
|
detail: { position }
|
|
});
|
|
element.dispatchEvent(dragEndEvent);
|
|
}
|
|
|
|
// Reset drag state
|
|
dragState.isDragging = false;
|
|
dragState.element = null;
|
|
}
|
|
|
|
// Clean up event listeners when changing modes or canceling
|
|
function cleanupDragListeners() {
|
|
document.removeEventListener("mousemove", handleDrag);
|
|
document.removeEventListener("mouseup", endDrag);
|
|
}
|
|
|
|
// Cancel editing
|
|
function cancelEdit() {
|
|
console.log("Canceling edit");
|
|
|
|
// Remove preview element
|
|
const previewElement = document.querySelector(".element-preview");
|
|
if (previewElement) {
|
|
previewElement.parentNode.removeChild(previewElement);
|
|
}
|
|
|
|
// Clean up drag listeners
|
|
cleanupDragListeners();
|
|
|
|
// Reset active element
|
|
editorState.activeElement = null;
|
|
|
|
// Reset form
|
|
const propertiesContent = document.getElementById("propertiesContent");
|
|
if (propertiesContent) {
|
|
propertiesContent.innerHTML = `
|
|
<div class="properties-placeholder">
|
|
<div class="placeholder-icon">
|
|
<i class="ph-light ph-cursor-click"></i>
|
|
</div>
|
|
<p>Click on the scene to create a new element, or on an existing element to edit it.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Hide buttons
|
|
document.getElementById("resetButton")?.classList.add("hide");
|
|
document.getElementById("deleteButton")?.classList.add("hide");
|
|
document.getElementById("buttons")?.classList.add("hide");
|
|
}
|
|
|
|
// Update element location after dragging
|
|
function updateElementLocation(event) {
|
|
if (!event.target || !event.detail) {
|
|
console.warn("Invalid event in updateElementLocation");
|
|
return;
|
|
}
|
|
|
|
const element = event.target;
|
|
const offset = event.detail.offset;
|
|
|
|
console.log("Updating element location with offset:", offset);
|
|
|
|
// Get the current position
|
|
const currentPosition = element.getAttribute("position") || { x: 0, y: 0, z: 0 };
|
|
|
|
// Calculate new position
|
|
const newPosition = {
|
|
x: currentPosition.x + offset.x,
|
|
y: currentPosition.y + offset.y,
|
|
z: currentPosition.z + offset.z
|
|
};
|
|
|
|
// Update element position
|
|
element.setAttribute("position", newPosition);
|
|
|
|
// Store the new position in editor state
|
|
editorState.currentPosition = {
|
|
x: newPosition.x,
|
|
y: newPosition.y,
|
|
z: newPosition.z
|
|
};
|
|
|
|
// If it's a curved image, update theta
|
|
if (element.tagName === "A-CURVEDIMAGE") {
|
|
const theta = cartesianToTheta(newPosition.x, newPosition.z);
|
|
element.setAttribute("theta-start", theta);
|
|
}
|
|
|
|
// Mark scene as modified
|
|
editorState.modified = true;
|
|
|
|
// Update coordinate display
|
|
const coordinateDisplay = document.getElementById("coordinateDisplay");
|
|
if (coordinateDisplay) {
|
|
coordinateDisplay.textContent = `X: ${newPosition.x.toFixed(2)}, Y: ${newPosition.y.toFixed(2)}, Z: ${newPosition.z.toFixed(2)}`;
|
|
}
|
|
|
|
// Update form values if they exist
|
|
const posXInput = document.getElementById("position_x");
|
|
const posYInput = document.getElementById("position_y");
|
|
const posZInput = document.getElementById("position_z");
|
|
|
|
if (posXInput) posXInput.value = newPosition.x.toFixed(2);
|
|
if (posYInput) posYInput.value = newPosition.y.toFixed(2);
|
|
if (posZInput) posZInput.value = newPosition.z.toFixed(2);
|
|
|
|
console.log("Position updated to:", newPosition);
|
|
}
|
|
|
|
// Save scene settings
|
|
function saveSceneSettings() {
|
|
console.log("Saving scene settings");
|
|
|
|
if (!editorState.scene) {
|
|
console.error("No scene data available");
|
|
return;
|
|
}
|
|
|
|
// Get form values
|
|
const title = document.getElementById("sceneTitle").value;
|
|
const description = document.getElementById("sceneDescription").value;
|
|
const defaultX = parseFloat(document.getElementById("defaultX").value) || 0;
|
|
const defaultY = parseFloat(document.getElementById("defaultY").value) || 0;
|
|
const defaultZ = parseFloat(document.getElementById("defaultZ").value) || 0;
|
|
const isPublic = document.getElementById("scenePublic").checked;
|
|
|
|
// Validate inputs
|
|
if (!title.trim()) {
|
|
showNotification("Please enter a scene title", "error");
|
|
document.getElementById("sceneTitle").focus();
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
const saveSettingsBtn = document.getElementById("saveSettingsBtn");
|
|
if (saveSettingsBtn) {
|
|
saveSettingsBtn.disabled = true;
|
|
saveSettingsBtn.innerHTML = '<i class="ph-light ph-spinner ph-spin"></i> Saving...';
|
|
}
|
|
|
|
// Create form data
|
|
const formData = new FormData();
|
|
formData.append("title", title);
|
|
formData.append("description", description);
|
|
formData.append("default_x", defaultX);
|
|
formData.append("default_y", defaultY);
|
|
formData.append("default_z", defaultZ);
|
|
formData.append("public", isPublic);
|
|
formData.append("base_content", editorState.scene.base_content.id);
|
|
formData.append("category", editorState.scene.category);
|
|
|
|
// Send API request
|
|
fetch(`/tours/api/scenes/${editorState.scene.id}/`, {
|
|
method: "PUT",
|
|
body: formData,
|
|
headers: {
|
|
"X-CSRFToken": getCookie("csrftoken")
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(data => {
|
|
throw new Error(JSON.stringify(data));
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Success
|
|
console.log("Scene settings saved successfully:", data);
|
|
showNotification("Scene settings saved successfully", "success");
|
|
|
|
// Update editor state
|
|
editorState.scene = data;
|
|
editorState.modified = false;
|
|
})
|
|
.catch(error => {
|
|
console.error("Error saving scene settings:", error);
|
|
let errorMessage = "Failed to save scene settings";
|
|
|
|
try {
|
|
const errorData = JSON.parse(error.message);
|
|
if (typeof errorData === 'object') {
|
|
errorMessage = Object.entries(errorData)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join(', ');
|
|
}
|
|
} catch (e) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
showNotification(errorMessage, "error");
|
|
})
|
|
.finally(() => {
|
|
// Reset button state
|
|
if (saveSettingsBtn) {
|
|
saveSettingsBtn.disabled = false;
|
|
saveSettingsBtn.innerHTML = '<i class="ph-light ph-check"></i> Save Settings';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Coordinate conversion functions
|
|
function cartesianToTheta(x, z) {
|
|
// Calculate the angle in radians
|
|
let angleRadians = Math.atan2(z, x);
|
|
|
|
// Convert to degrees
|
|
let angleDegrees = angleRadians * (180 / Math.PI);
|
|
|
|
// A-Frame's thetaStart is measured from the positive X-axis and goes counter-clockwise
|
|
let thetaStart = 90 - angleDegrees;
|
|
|
|
// Normalize to 0 - 360
|
|
thetaStart = thetaStart < 0 ? thetaStart + 360 : thetaStart;
|
|
|
|
return thetaStart;
|
|
}
|
|
|
|
function thetaToCartesian(thetaStart, radius = 1) {
|
|
// Convert thetaStart back to standard Cartesian coordinates
|
|
let angleDegrees = 90 - thetaStart;
|
|
|
|
// Normalize to 0 - 360
|
|
angleDegrees = angleDegrees % 360;
|
|
if (angleDegrees < 0) {
|
|
angleDegrees += 360;
|
|
}
|
|
|
|
// Convert to radians
|
|
let angleRadians = angleDegrees * (Math.PI / 180);
|
|
|
|
// Calculate Cartesian coordinates
|
|
let x = radius * Math.cos(angleRadians);
|
|
let z = radius * Math.sin(angleRadians);
|
|
|
|
return { x, z };
|
|
}
|
|
|
|
function cartesianToSpherical(x, y, z) {
|
|
// Convert Cartesian (x,y,z) to Spherical (radius, phi, theta)
|
|
const radius = Math.sqrt(x * x + y * y + z * z);
|
|
|
|
// Handle the case where radius is zero or very small
|
|
if (radius < 0.001) {
|
|
return { radius: 0, phi: 0, theta: 0 };
|
|
}
|
|
|
|
// Phi is the angle from the y-axis (0 to PI)
|
|
const phi = Math.acos(y / radius);
|
|
|
|
// Theta is the angle in the xz plane from the positive x-axis (-PI to PI)
|
|
const theta = Math.atan2(z, x);
|
|
|
|
return { radius, phi, theta };
|
|
}
|
|
|
|
function sphericalToCartesian(radius, phi, theta) {
|
|
// Convert Spherical (radius, phi, theta) to Cartesian (x,y,z)
|
|
// phi: angle from y-axis (0 to PI)
|
|
// theta: angle in xz plane from positive x-axis (-PI to PI)
|
|
|
|
const x = radius * Math.sin(phi) * Math.cos(theta);
|
|
const y = radius * Math.cos(phi);
|
|
const z = radius * Math.sin(phi) * Math.sin(theta);
|
|
|
|
return { x, y, z };
|
|
}
|
|
|
|
// Constrain a point to a sphere with given radius
|
|
function constrainToSphere(x, y, z, radius = 5) {
|
|
const { phi, theta } = cartesianToSpherical(x, y, z);
|
|
return sphericalToCartesian(radius, phi, theta);
|
|
}
|
|
|
|
// Find parent quackscape-scene element
|
|
function findParentScene(element) {
|
|
let parent = element.parentElement;
|
|
|
|
while (parent && parent.tagName !== "QUACKSCAPE-SCENE") {
|
|
parent = parent.parentElement;
|
|
}
|
|
|
|
return parent;
|
|
}
|
|
|
|
// Reload the current scene
|
|
function reloadScene() {
|
|
console.log("Reloading scene");
|
|
|
|
const sceneElement = document.querySelector("quackscape-scene");
|
|
if (!sceneElement) {
|
|
console.error("Quackscape scene element not found");
|
|
return;
|
|
}
|
|
|
|
const sceneId = sceneElement.getAttribute("id");
|
|
const x = parseInt(sceneElement.getAttribute("x")) || 0;
|
|
const y = parseInt(sceneElement.getAttribute("y")) || 0;
|
|
const z = parseInt(sceneElement.getAttribute("z")) || 0;
|
|
|
|
// Show loading indicator
|
|
sceneElement.showLoadingIndicator?.();
|
|
|
|
// Reload scene
|
|
window.loadScene(sceneId, x, y, z, sceneElement, sceneElement.hasAttribute("embedded"))
|
|
.then(() => {
|
|
console.log("Scene reloaded successfully");
|
|
|
|
// Update element count
|
|
updateElementCount();
|
|
|
|
// Populate elements list
|
|
populateElementsList();
|
|
|
|
// Hide loading indicator
|
|
sceneElement.hideLoadingIndicator?.();
|
|
|
|
// Refresh scene data
|
|
getScene(sceneId).then(response => {
|
|
editorState.scene = response.obj;
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error("Error reloading scene:", error);
|
|
sceneElement.showErrorIndicator?.(error);
|
|
});
|
|
}
|
|
|
|
// Update element count display
|
|
function updateElementCount() {
|
|
const elementCount = document.getElementById("elementCount");
|
|
if (elementCount && editorState.scene) {
|
|
const count = editorState.scene.elements.length;
|
|
elementCount.textContent = `${count} element${count !== 1 ? 's' : ''}`;
|
|
}
|
|
}
|
|
|
|
// Populate elements list in sidebar
|
|
function populateElementsList() {
|
|
const elementsList = document.querySelector(".elements-list");
|
|
if (!elementsList || !editorState.scene) {
|
|
console.warn("Elements list or scene data not available");
|
|
return;
|
|
}
|
|
|
|
// Clear existing elements
|
|
elementsList.innerHTML = "";
|
|
|
|
if (!editorState.scene.elements || editorState.scene.elements.length === 0) {
|
|
elementsList.innerHTML = `
|
|
<div class="empty-elements">
|
|
<p>No elements added yet</p>
|
|
<p>Click on the scene to add elements</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Add each element to the list
|
|
editorState.scene.elements.forEach(element => {
|
|
const elementItem = document.createElement("div");
|
|
elementItem.className = "element-item";
|
|
elementItem.dataset.elementId = element.id;
|
|
|
|
// Determine element type icon
|
|
let iconClass = "ph-light ph-cube";
|
|
if (element.data.tag === "a-curvedimage") {
|
|
if (element.data.attributes.onclick) {
|
|
iconClass = "ph-light ph-portal";
|
|
} else {
|
|
iconClass = "ph-light ph-image-square";
|
|
}
|
|
} else if (element.data.tag === "a-entity") {
|
|
iconClass = "ph-light ph-map-pin";
|
|
}
|
|
|
|
elementItem.innerHTML = `
|
|
<div class="element-icon">
|
|
<i class="${iconClass}"></i>
|
|
</div>
|
|
<div class="element-info">
|
|
<div class="element-title">${element.data.attributes.alt || "Unnamed Element"}</div>
|
|
<div class="element-type">${getElementTypeName(element.data.tag, element.data.attributes)}</div>
|
|
</div>
|
|
<div class="element-actions">
|
|
<button class="btn btn-icon btn-sm element-edit-btn" title="Edit">
|
|
<i class="ph-light ph-pencil-simple"></i>
|
|
</button>
|
|
<button class="btn btn-icon btn-sm element-delete-btn" title="Delete">
|
|
<i class="ph-light ph-trash"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
// Add click handlers
|
|
const editBtn = elementItem.querySelector(".element-edit-btn");
|
|
if (editBtn) {
|
|
editBtn.addEventListener("click", () => {
|
|
const elementInScene = document.getElementById(element.id);
|
|
if (elementInScene) {
|
|
// Simulate click on element
|
|
elementInScene.dispatchEvent(new MouseEvent("click"));
|
|
} else {
|
|
console.warn("Element not found in scene:", element.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
const deleteBtn = elementItem.querySelector(".element-delete-btn");
|
|
if (deleteBtn) {
|
|
deleteBtn.addEventListener("click", () => {
|
|
const elementInScene = document.getElementById(element.id);
|
|
if (elementInScene) {
|
|
// Set as active element and delete
|
|
editorState.activeElement = elementInScene;
|
|
deleteElement();
|
|
} else {
|
|
console.warn("Element not found in scene for deletion:", element.id);
|
|
}
|
|
});
|
|
}
|
|
|
|
elementsList.appendChild(elementItem);
|
|
});
|
|
}
|
|
|
|
// Get friendly element type name
|
|
function getElementTypeName(tag, attributes) {
|
|
if (tag === "a-curvedimage") {
|
|
return attributes.onclick ? "Teleport" : "Image";
|
|
} else if (tag === "a-entity") {
|
|
return "Marker";
|
|
}
|
|
return tag.replace("a-", "");
|
|
}
|
|
|
|
// Set up tab listeners
|
|
function setupTabListeners() {
|
|
const tabs = document.querySelectorAll('[data-bs-toggle="tab"]');
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener("click", function () {
|
|
const target = document.querySelector(this.dataset.bsTarget);
|
|
if (!target) {
|
|
console.warn("Tab target not found:", this.dataset.bsTarget);
|
|
return;
|
|
}
|
|
|
|
// Hide all tab panes
|
|
document.querySelectorAll(".tab-pane").forEach(pane => {
|
|
pane.classList.remove("show", "active");
|
|
});
|
|
|
|
// Show selected tab pane
|
|
target.classList.add("show", "active");
|
|
|
|
// Update active tab
|
|
tabs.forEach(t => t.classList.remove("active"));
|
|
this.classList.add("active");
|
|
});
|
|
});
|
|
}
|
|
|
|
// Save all changes to the scene
|
|
function saveChanges() {
|
|
console.log("Saving all changes");
|
|
|
|
if (!editorState.scene) {
|
|
console.error("No scene data available");
|
|
return;
|
|
}
|
|
|
|
// Get scene settings
|
|
const title = document.getElementById("sceneTitle")?.value || editorState.scene.title;
|
|
const description = document.getElementById("sceneDescription")?.value || editorState.scene.description;
|
|
const defaultX = parseFloat(document.getElementById("defaultX")?.value) || editorState.scene.default_x || 0;
|
|
const defaultY = parseFloat(document.getElementById("defaultY")?.value) || editorState.scene.default_y || 0;
|
|
const defaultZ = parseFloat(document.getElementById("defaultZ")?.value) || editorState.scene.default_z || 0;
|
|
const isPublic = document.getElementById("scenePublic")?.checked !== undefined ?
|
|
document.getElementById("scenePublic").checked : editorState.scene.public;
|
|
|
|
// Create form data
|
|
const formData = new FormData();
|
|
formData.append("title", title);
|
|
formData.append("description", description);
|
|
formData.append("default_x", defaultX);
|
|
formData.append("default_y", defaultY);
|
|
formData.append("default_z", defaultZ);
|
|
formData.append("public", isPublic);
|
|
formData.append("base_content", editorState.scene.base_content.id);
|
|
formData.append("category", editorState.scene.category);
|
|
|
|
// Show loading state
|
|
const saveBtn = document.getElementById("saveBtn");
|
|
if (saveBtn) {
|
|
saveBtn.disabled = true;
|
|
saveBtn.innerHTML = '<i class="ph-light ph-spinner ph-spin"></i> Saving...';
|
|
}
|
|
|
|
// Send API request
|
|
fetch(`/tours/api/scenes/${editorState.scene.id}/`, {
|
|
method: "PUT",
|
|
body: formData,
|
|
headers: {
|
|
"X-CSRFToken": getCookie("csrftoken")
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(data => {
|
|
throw new Error(JSON.stringify(data));
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Success
|
|
console.log("Scene saved successfully:", data);
|
|
showNotification("Scene saved successfully", "success");
|
|
|
|
// Update editor state
|
|
editorState.scene = data;
|
|
editorState.modified = false;
|
|
|
|
// Store original data for future comparisons
|
|
editorState.originalData = JSON.parse(JSON.stringify(data));
|
|
})
|
|
.catch(error => {
|
|
console.error("Error saving scene:", error);
|
|
let errorMessage = "Failed to save scene";
|
|
|
|
try {
|
|
const errorData = JSON.parse(error.message);
|
|
if (typeof errorData === 'object') {
|
|
errorMessage = Object.entries(errorData)
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join(', ');
|
|
}
|
|
} catch (e) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
showNotification(errorMessage, "error");
|
|
})
|
|
.finally(() => {
|
|
// Reset button state
|
|
if (saveBtn) {
|
|
saveBtn.disabled = false;
|
|
saveBtn.innerHTML = '<i class="ph-light ph-floppy-disk"></i> Save';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Preview the scene
|
|
function previewScene() {
|
|
console.log("Previewing scene");
|
|
|
|
if (!editorState.scene) {
|
|
console.error("No scene data available");
|
|
return;
|
|
}
|
|
|
|
// Check if there are unsaved changes
|
|
if (editorState.modified) {
|
|
const confirmPreview = confirm("You have unsaved changes. Save before previewing?");
|
|
if (confirmPreview) {
|
|
// Save changes first, then preview
|
|
saveChanges();
|
|
setTimeout(() => {
|
|
window.open(`/tours/scene/${editorState.scene.id}/`, "_blank");
|
|
}, 1000);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Open preview in new tab
|
|
window.open(`/tours/scene/${editorState.scene.id}/`, "_blank");
|
|
}
|
|
|
|
// Reset view to default
|
|
function resetView() {
|
|
console.log("Resetting view");
|
|
|
|
if (!editorState.scene) {
|
|
console.error("No scene data available");
|
|
return;
|
|
}
|
|
|
|
const camera = document.querySelector("a-camera");
|
|
if (!camera) {
|
|
console.error("Camera element not found");
|
|
return;
|
|
}
|
|
|
|
const rig = camera.parentElement;
|
|
if (!rig) {
|
|
console.error("Camera rig not found");
|
|
return;
|
|
}
|
|
|
|
// Reset camera rotation
|
|
const defaultX = editorState.scene.default_x || 0;
|
|
const defaultY = editorState.scene.default_y || 0;
|
|
const defaultZ = editorState.scene.default_z || 0;
|
|
|
|
rig.setAttribute("rotation", {
|
|
x: defaultX,
|
|
y: defaultY,
|
|
z: defaultZ
|
|
});
|
|
|
|
showNotification("View reset to default", "info");
|
|
}
|
|
|
|
// Toggle fullscreen
|
|
function toggleFullscreen() {
|
|
console.log("Toggling fullscreen");
|
|
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen().catch(err => {
|
|
console.error("Error attempting to enable fullscreen:", err);
|
|
showNotification(`Error enabling fullscreen: ${err.message}`, "error");
|
|
});
|
|
} else {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debug function to toggle debug visibility
|
|
function toggleDebugVisibility() {
|
|
const debugElement = document.getElementById("propertiesDebug");
|
|
if (debugElement) {
|
|
debugElement.classList.toggle("hide");
|
|
}
|
|
}
|
|
|
|
// Add keyboard shortcut for debug mode
|
|
document.addEventListener("keydown", function (event) {
|
|
if (event.ctrlKey && event.shiftKey && event.key === "D") {
|
|
toggleDebugVisibility();
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Export necessary functions
|
|
export { initEditor }; |