quackscape/assets/js/editor.js
Kumi c43ddfef2a
fix(editor): correct TeleportElement option and improve dropdown
Correctly set the value for the Teleport option in the element creation
form, ensuring that elements are correctly classified. Enhanced dynamic
data fetching for scenes and categories to populate the destination
dropdown based on the scene's category, improving user experience in
specifying teleport destinations. Resolved an issue with event
propagation by explicitly setting the button type for dropdown items,
preventing form submission on selection. This change streamlines the
process of creating teleport elements and selecting destinations within
the editor, making it more intuitive and error-free.

- Removed outdated TODO comment related to element drags handling.
- Removed unused rotation input fields for a cleaner UI.
- Added conversion function `thetaToCartesian` for future
functionalities.
2024-03-28 12:27:10 +01:00

363 lines
12 KiB
JavaScript

import { getScene, getSceneElement, getCategory, getCookie } from "./api";
import { populateDestinationDropdown } from "./editor/teleport";
import registerClickDrag from "@kumitterer/aframe-click-drag-component";
registerClickDrag(window.aframe);
import "../css/editor.css";
let clickTimestamp = 0;
// Find parent quackscape-scene for ID
function findParentScene(element) {
var parent = element.parentElement;
while (parent.tagName != "QUACKSCAPE-SCENE") {
parent = parent.parentElement;
}
return parent;
}
// Distinguishing clicks from drags based on duration.
// TODO: Find a better way to distinguish these.
function addEventListeners(element) {
element.addEventListener("mousedown", function (event) {
clickTimestamp = event.timeStamp;
});
element.addEventListener("mouseup", function (event) {
if (event.timeStamp - clickTimestamp > 200) {
// Ignoring this, we only handle regular short clicks.
return;
} else {
handleClick(event);
}
// Right-clicks are definitely intentional.
element.addEventListener("contextmenu", function (event) {
handleClick(event);
});
});
}
// Open a modal for creating a new Element
function startCreateElement(event) {
var propertiesTitle = document.getElementById("propertiesTitle");
propertiesTitle.textContent = "Create Element";
var propertiesContent = document.getElementById("propertiesContent");
var thetaStart = cartesianToTheta(
event.detail.intersection.point.x,
event.detail.intersection.point.z
);
propertiesContent.innerHTML = `
<div class="hide" id="propertiesDebug">
<b>Creating element at:</b><br/>
X: ${event.detail.intersection.point.x}<br/>
Y: ${event.detail.intersection.point.y}<br/>
Z: ${event.detail.intersection.point.z}<br/>
Calculated Theta: ${thetaStart}<br/>
Parent Element Type: ${event.srcElement.tagName}<br/>
<hr/>
</div>
<form id="newElementForm">
<label for="resourcetype" class="form-label">Element Type</label>
<select class="form-control" aria-label="Element Type" id="resourcetype">
<option selected>Select Element Type</option>
<option value="MarkerElement">Marker</option>
<option value="ImageElement">Image</option>
<option value="TeleportElement">Teleport</option>
</select>
<input type="hidden" id="csrfmiddlewaretoken" value="${getCookie(
"csrftoken"
)}">
<div id="elementProperties"></div>
</form>
`;
var scene = findParentScene(event.target);
var scene_data_request = getScene(scene.getAttribute("id"));
document.getElementById("resetButton").classList.remove("hide");
document.getElementById("buttons").classList.remove("hide");
document
.getElementById("resourcetype")
.addEventListener("change", function () {
var selectedType = document.getElementById("resourcetype").value;
createElementPropertiesForm(selectedType, event, thetaStart);
scene_data_request.then((scene_data) => {
var category_data_request = getCategory(scene_data.obj.category);
category_data_request.then((category_data) => {
populateDestinationDropdown(null, category_data);
});
});
});
}
function createElementPropertiesForm(elementType) {
const elementProperties = document.getElementById("elementProperties");
let inputFields = "";
switch (elementType) {
case "MarkerElement":
case "ImageElement":
case "TeleportElement":
inputFields = `
<div class="mb-2">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" placeholder="Title">
</div>
<div class="mb-2">
<label for="position_x" class="form-label">Position</label>
<div class="row g-2">
<div class="col">
<input type="number" class="form-control" id="position_x" name="position_x" min="0" max="360" placeholder="X">
</div>
<div class="col">
<input type="number" class="form-control" id="position_y" name="position_y" min="0" max="360" placeholder="Y">
</div>
<div class="col">
<input type="number" class="form-control" id="position_z" name="position_z" min="0" max="360" placeholder="Z">
</div>
</div>
</div>
`;
if (elementType !== "MarkerElement") {
inputFields += `
<div class="mb-2">
<label for="elementUpload" class="form-label">Image</label>
<input id="elementUpload" type="file" class="form-control">
</div>`;
}
if (elementType === "TeleportElement") {
inputFields += `
<div class="dropdown mb-2">
<label for="destinationDropdownSearch" class="form-label">Teleport Destination</label>
<input class="form-control" autocomplete="off" id="destinationDropdownSearch" type="text" placeholder="Search...">
<input type="hidden" id="destination">
<div class="dropdown-menu" id="destinationDropdownMenu">
<!-- Dropdown items will be populated here by JavaScript -->
</div>
</div>
<div class="mb-2">
<label for="destination_x" class="form-label">Destination X/Y/Z Rotation</label>
<div class="row g-2">
<div class="col">
<input type="number" class="form-control" id="destination_x" name="destination_x" min="0" max="360" placeholder="X">
</div>
<div class="col">
<input type="number" class="form-control" id="destination_y" name="destination_y" min="0" max="360" placeholder="Y">
</div>
<div class="col">
<input type="number" class="form-control" id="destination_z" name="destination_z" min="0" max="360" placeholder="Z">
</div>
</div>
</div>
</div>
`;
}
break;
default:
// TODO: Handle unknown element type
}
elementProperties.innerHTML = inputFields;
}
function startModifyElement(event) {
// 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", function (event) {
console.log(event); // TODO: Display, obviously.
});
var propertiesTitle = document.getElementById("propertiesTitle");
propertiesTitle.textContent = "Modify Element";
// Get element from API
var scene = findParentScene(event.target);
var element_data_request = getSceneElement(
scene.getAttribute("id"),
event.target.getAttribute("id")
);
element_data_request.then((element_data) => {
var propertiesContent = document.getElementById("propertiesContent");
propertiesContent.innerHTML = `
<div class="hide" id="propertiesDebug">
<b>Modifying element:</b><br/>
Element Type: ${event.srcElement.tagName}<br/>
Element ID: ${event.target.getAttribute("id")}<br/>
Element data: ${JSON.stringify(element_data.obj)}<br/>
<hr/>
</div>
<form id="modifyElementForm">
<input type="hidden" id="csrfmiddlewaretoken" value="${getCookie(
"csrftoken"
)}">
<input type="hidden" id="id" value="${event.target.getAttribute("id")}">
<div id="elementProperties"></div>
</form>
`;
createElementPropertiesForm(element_data.obj.resourcetype);
var scene_data_request = getScene(scene.getAttribute("id"));
scene_data_request.then((scene_data) => {
var category_data_request = getCategory(scene_data.obj.category);
category_data_request.then((category_data) => {
populateDestinationDropdown(
element_data.obj.destination,
category_data
);
});
});
document
.getElementById("elementUpload")
.addEventListener("change", function () {
if (this.files && this.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
// Set the image to the file contents
if (event.target.getAttribute("data-original-src") == undefined) {
event.target.setAttribute(
"data-original-src",
event.target.getAttribute("src")
);
}
event.target.setAttribute("src", e.target.result);
};
reader.readAsDataURL(this.files[0]);
}
});
if (element_data.obj.destination_x != -1) {
document.getElementById("destination_x").value =
element_data.obj.destination_x;
}
if (element_data.obj.destination_y != -1) {
document.getElementById("destination_y").value =
element_data.obj.destination_y;
}
if (element_data.obj.destination_z != -1) {
document.getElementById("destination_z").value =
element_data.obj.destination_z;
}
document.getElementById("resetButton").classList.remove("hide");
document.getElementById("buttons").classList.remove("hide");
});
}
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 ("right" direction)
// and goes counter-clockwise, so this should directly give us the thetaStart value.
let thetaStart = 90 - angleDegrees;
// Since atan2 returns values from -180 to 180, let's normalize this to 0 - 360
thetaStart = thetaStart < 0 ? thetaStart + 360 : thetaStart;
return thetaStart;
}
function thetaToCartesian(thetaStart, radius = 1) {
// Convert thetaStart back to the angle in degrees as used in standard Cartesian coordinates.
let angleDegrees = 90 - thetaStart;
// Normalize the angle to be within the range of 0 to 360 degrees
angleDegrees = angleDegrees % 360;
if (angleDegrees < 0) {
angleDegrees += 360;
}
// Convert the angle back to radians
let angleRadians = angleDegrees * (Math.PI / 180);
// Calculate the Cartesian coordinates using the angle and radius
// x = r * cos(θ)
let x = radius * Math.cos(angleRadians);
// z = r * sin(θ)
let z = radius * Math.sin(angleRadians);
return { x, z };
}
function latLonToXYZ(lat, lon, radius = 5) {
// Convert lat/lon to X/Y/Z coordinates on the sphere
const phi = (90 - lat) * (Math.PI / 180);
const theta = (lon + 180) * (Math.PI / 180);
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 };
}
function handleClick(event) {
// If clicked on the sky, start creating a new element
if (event.target.tagName == "A-SKY") {
startCreateElement(event);
} else {
// Else we are modifying an existing element
startModifyElement(event);
}
}
document.addEventListener("loadedQuackscapeScene", function (event) {
// Get the scene
var scene = document.querySelector("a-scene");
// Get all children
var children = scene.children;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.tagName.startsWith("A-")) {
// Remove original onclick events
if (child.hasAttribute("onclick")) {
child.removeAttribute("onclick");
}
// Add new event listeners
addEventListeners(child);
}
}
});
function toggleDebugVisibility() {
const debugElement = document.getElementById("propertiesDebug");
debugElement.classList.toggle("hide");
}
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.shiftKey && event.key === "Q") {
toggleDebugVisibility();
event.preventDefault();
}
});