quackscape/assets/js/editor.js
Kumi ac2c4e788f
feat(editor): implement element location update on
dragend

Replaced a placeholder console.log with a concrete implementation of the
`updateElementLocation` function in the `startModifyElement` event
listener. This function calculates a new angular position (`theta`)
based on the element's offset after dragging it in the editor and
updates the element's `theta-start` attribute accordingly. This change
leads to a more dynamic and interactive user experience in the editor by
allowing users to reposition elements visually and have those changes
reflected immediately.
2024-03-28 13:09:48 +01:00

369 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) {
updateElementLocation(event);
});
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 updateElementLocation(event) {
const theta = cartesianToTheta(event.detail.offset.x, event.detail.offset.z);
event.target.setAttribute("theta-start", theta);
}
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();
}
});