Kumi
538ddd76e9
Wrapped element creation and modification properties sections in a div with a `hide` class, which can now be toggled via a keyboard shortcut (Ctrl+Shift+Q). This debug mode allows developers to easily view and hide debugging information about elements being created or modified in the editor. This enhancement is aimed at improving the development and troubleshooting process by providing quick access to relevant debug information without cluttering the UI for users. Additionally, a new function `toggleDebugVisibility` was added to manage the visibility of debug information, and a keyboard event listener was implemented to handle the specified keyboard shortcut. This update aims to balance the need for detailed debugging information during development with a clean, user-friendly interface during regular use.
345 lines
No EOL
11 KiB
JavaScript
345 lines
No EOL
11 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.
|
|
// TODO: Find a way to handle drags of elements
|
|
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="ImageElement">Teleport</option>
|
|
</select>
|
|
<input type="hidden" id="csrfmiddlewaretoken" value="${getCookie(
|
|
"csrftoken"
|
|
)}">
|
|
<div id="elementProperties"></div>
|
|
</form>
|
|
`;
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
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>
|
|
<div class="mb-2">
|
|
<label for="rotation_x" class="form-label">Rotation</label>
|
|
<div class="row g-2">
|
|
<div class="col">
|
|
<input type="number" class="form-control" id="rotation_x" name="rotation_x" min="0" max="360" placeholder="X">
|
|
</div>
|
|
<div class="col">
|
|
<input type="number" class="form-control" id="rotation_y" name="rotation_y" min="0" max="360" placeholder="Y">
|
|
</div>
|
|
<div class="col">
|
|
<input type="number" class="form-control" id="rotation_z" name="rotation_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 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();
|
|
}
|
|
}); |