diff --git a/.gitignore b/.gitignore index b82490291..e43b1ce98 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ !/plugins/csp-satellites !/plugins/csp-sharad !/plugins/csp-simple-bodies +!/plugins/csp-simple-objects !/plugins/csp-stars !/plugins/csp-timings !/plugins/csp-trajectories diff --git a/plugins/csp-simple-objects/.gitignore b/plugins/csp-simple-objects/.gitignore new file mode 100644 index 000000000..8e062ad38 --- /dev/null +++ b/plugins/csp-simple-objects/.gitignore @@ -0,0 +1 @@ +*.drawio diff --git a/plugins/csp-simple-objects/CMakeLists.txt b/plugins/csp-simple-objects/CMakeLists.txt new file mode 100644 index 000000000..380352ff5 --- /dev/null +++ b/plugins/csp-simple-objects/CMakeLists.txt @@ -0,0 +1,46 @@ +# ------------------------------------------------------------------------------------------------ # +# This file is part of CosmoScout VR # +# and may be used under the terms of the MIT license. See the LICENSE file for details. # +# Copyright: (c) 2022 German Aerospace Center (DLR) # +# ------------------------------------------------------------------------------------------------ # + +option(CSP_SIMPLE_OBJECTS "Enable compilation of this plugin" ON) + +if (NOT CSP_SIMPLE_OBJECTS) + return() +endif() + +# build plugin ------------------------------------------------------------------------------------- + +file(GLOB SOURCE_FILES src/*.cpp) + +# Resoucre files and header files are only added in order to make them available in your IDE. +file(GLOB HEADER_FILES src/*.hpp) +file(GLOB_RECURSE RESOUCRE_FILES gui/*) + +add_library(csp-simple-objects SHARED + ${SOURCE_FILES} + ${HEADER_FILES} + ${RESOUCRE_FILES} +) + +target_link_libraries(csp-simple-objects + PUBLIC + cs-core +) + +# Add this Plugin to a "plugins" folder in your IDE. +set_property(TARGET csp-simple-objects PROPERTY FOLDER "plugins") + +# We mark all resource files as "header" in order to make sure that no one tries to compile them. +set_source_files_properties(${RESOUCRE_FILES} PROPERTIES HEADER_FILE_ONLY TRUE) + +# Make directory structure available in your IDE. +source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES + ${SOURCE_FILES} ${HEADER_FILES} ${RESOUCRE_FILES} +) + +# install plugin ----------------------------------------------------------------------------------- + +install(TARGETS csp-simple-objects DESTINATION "share/plugins") +install(DIRECTORY "gui" DESTINATION "share/resources") \ No newline at end of file diff --git a/plugins/csp-simple-objects/LICENSE b/plugins/csp-simple-objects/LICENSE new file mode 100644 index 000000000..3e07ab62e --- /dev/null +++ b/plugins/csp-simple-objects/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 German Aerospace Center (DLR) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/csp-simple-objects/README.md b/plugins/csp-simple-objects/README.md new file mode 100644 index 000000000..83e5c791f --- /dev/null +++ b/plugins/csp-simple-objects/README.md @@ -0,0 +1,94 @@ +# Simple Objects for CosmoScout VR + +A CosmoScout VR plugin to import glTF Models, place them and edit their configuration. + + +![](demo_image.png) + + + + + + +## Configuration + +Import all `.gltf` model files to /resources/models and your environment cube maps to /resources/textures. +The files will automatically be copied to the installation path, as you build and install CosmoScout VR. + + +Add a `csp-simple-objects` entry to the plugins in your `settings.json`. +There you can specify all objects and their attributes. +Here is a short example describing possible attributes: + +``` +{ + "plugins": { + ... [other plugins] + "csp-simple-objects": { + "objects": { + "": { + "modelFile": "", + "environmentMap": "", + "anchor": "", + "lngLat": [ , ], + + Optional parameters: + "elevation": , + "scale": , + "diagonalLength": , + "rotation": [, , , ], + "alignToSurface": + }, + ... [more objects] + } + } + } +} +``` + + + + +## Example + +To achieve the same result as in the demo image above, add this section to your plugins in the scene configuration. + +Here you can find the glTF models of the [Rover](https://mars.nasa.gov/resources/24584/curiosity-rover-3d-model/) and [Avocado](https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/Avocado/glTF-Binary/Avocado.glb). +The Mars environment map is available in the [textures folder of csp-satellites](../csp-satellites/textures/marsEnvMap.dds). + + +```json +"csp-simple-objects": { + "objects": { + "Avocado": { + "modelFile": "../share/resources/models/Avocado.glb", + "environmentMap": "../share/resources/textures/marsEnvMap.dds", + "anchor": "Mars", + "lngLat": [ + 137.39096509050674, + -4.7309278444763345 + ], + "elevation": 0.5, + "scale": 40, + "rotation": [ + 0.5, 0.5, 0.5, -0.5 + ], + "alignToSurface": true + }, + "Curiosity Rover": { + "modelFile": "../share/resources/models/Curiosity_static.glb", + "environmentMap": "../share/resources/textures/marsEnvMap.dds", + "anchor":"Mars", + "lngLat": [ + 137.39091093946817, + -4.730935596266577 + ], + "elevation": 0.1, + "rotation": [ + 0, 0.531, 0, 0.847 + ], + "alignToSurface": true + } + } +} +``` diff --git a/plugins/csp-simple-objects/demo_image.png b/plugins/csp-simple-objects/demo_image.png new file mode 100644 index 000000000..4f02023bc Binary files /dev/null and b/plugins/csp-simple-objects/demo_image.png differ diff --git a/plugins/csp-simple-objects/gui/css/csp-simple-objects.css b/plugins/csp-simple-objects/gui/css/csp-simple-objects.css new file mode 100644 index 000000000..b6563adc6 --- /dev/null +++ b/plugins/csp-simple-objects/gui/css/csp-simple-objects.css @@ -0,0 +1,50 @@ +#simple-objects-editor .window-wrapper { + width: 520px; +} + +#simple-objects-editor .form-control.is-invalid { + padding-right: 7px; + background: none; +} + +#simple-objects-editor .disabled:hover { + /*background-color: rgba(230, 230, 255, 0.1); + border-bottom: 2px solid var(--cs-color-primary); + box-shadow: none; + color: var(--cs-color-text);*/ + pointer-events: none; +} + +#simple-objects-editor .btn.fix-rounded-right { + height: calc(1.5em + .75rem + 2px); +} + +#simple-objects-editor .filter-option { + height: auto; + padding: .375rem .75rem; +} + +#simple-objects-editor label { + padding: .375rem .75rem; +} + +#simple-objects-editor-pick-location { + height: calc(1.5em + .75rem + 2px); +} + +#simple-objects-editor-pick-location > i { + top: -2px; +} + +#simple-objects-editor-pick-location.active { + color: var(--cs-color-text-highlight); + text-shadow: var(--cs-text-glow); + border: 1px dashed var(--cs-color-primary-transparent); + border-bottom: 2px solid var(--cs-color-text-highlight); + background-color: var(--cs-color-background-lightest); +} + +#simple-objects-editor-pick-location.disabled { + top: 0px; + pointer-events: none; +} diff --git a/plugins/csp-simple-objects/gui/js/csp-simple-objects-editor.js b/plugins/csp-simple-objects/gui/js/csp-simple-objects-editor.js new file mode 100644 index 000000000..801705f58 --- /dev/null +++ b/plugins/csp-simple-objects/gui/js/csp-simple-objects-editor.js @@ -0,0 +1,759 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +(() => { + + class SimpleObjectsEditorApi extends IApi { + /** + * @inheritDoc + */ + name = 'simpleObjectsEditor'; + + // Buttons + _pickLocationButton; + _saveButton; + _deleteButton; + + // Input fields + _nameDiv; + _anchorDiv; + _modelSelect; + _mapSelect; + _modelButton; + _mapButton; + _locationLngDiv; + _locationLatDiv; + _elevationDiv; + _scaleDiv; + _sizeDiv; + _rotationXDiv; + _rotationYDiv; + _rotationZDiv; + _rotationWDiv; + _alignSurfaceDiv; + _nothingGivenError; + + // Window variables + _editor; + _title; + _editorOpen = false; + _editObjectName = null; + _objectNames = new Set(); // contains all known object names + + // Pick location related variables + _lastHeight = Infinity; + _pickLocationButtonActive = false; + + // Tab functionality maps + _tabFwdMap = new Map(); + //tabRevMap = new Map(); + + /** + * @inheritDoc + */ + init() { + this._title = document.getElementById("simple-objects-editor-title"); + this._editor = document.getElementById("simple-objects-editor"); + this._nothingGivenError = document.getElementById("simple-objects-editor-nothing-given-error"); + this._pickLocationButton = document.getElementById("simple-objects-editor-pick-location"); + this._saveButton = document.getElementById("simple-objects-editor-save-button"); + this._deleteButton = document.getElementById("simple-objects-editor-delete-button"); + this._nameDiv = document.getElementById("simple-objects-editor-name"); + this._anchorDiv = document.getElementById("simple-objects-editor-anchor"); + this._modelSelect = document.getElementById("simple-objects-editor-model-file"); + this._mapSelect = document.getElementById("simple-objects-editor-map-file"); + this._locationLngDiv = document.getElementById("simple-objects-editor-location-lng"); + this._locationLatDiv = document.getElementById("simple-objects-editor-location-lat"); + this._elevationDiv = document.getElementById("simple-objects-editor-elevation"); + this._scaleDiv = document.getElementById("simple-objects-editor-scale"); + this._sizeDiv = document.getElementById("simple-objects-editor-size"); + this._rotationXDiv = document.getElementById("simple-objects-editor-rotation-x"); + this._rotationYDiv = document.getElementById("simple-objects-editor-rotation-y"); + this._rotationZDiv = document.getElementById("simple-objects-editor-rotation-z"); + this._rotationWDiv = document.getElementById("simple-objects-editor-rotation-w"); + this._alignSurfaceDiv = document.getElementById("simple-objects-editor-align-surface"); + + /** + * Is only used for the dropdown buttons that need to be initialized in advance. + * The initalization adds the inner button HTMLElement after which a div with + * the invalid-feedback class needs to be added to display a false input correctly. + * @param {HTMLElement} div Button HTMLElement after which the invalid-feedback div should be added to. + */ + let addInvalidFeedback = (div) => { + if (div.parentNode.querySelector(".invalid-feedback") == null) { + let el = document.createElement("div"); + el.classList.add("invalid-feedback"); + div.after(el); + } + } + + // Dropdown needs to be initialized before invalid-feedback class is added to the button divs + CosmoScout.gui.initDropDowns(); + + this._modelButton = document.querySelector("#simple-objects-editor-model-file + button"); + this._mapButton = document.querySelector("#simple-objects-editor-map-file + button"); + this._modelButton.classList.add("form-control"); + this._mapButton.classList.add("form-control"); + + addInvalidFeedback(this._modelButton); + addInvalidFeedback(this._mapButton); + + + /** + * Tab loop initilization. + * Only forward tabbing works (see below). + * + * + * Funktionalität für rückwärts tabben durch prevDiv und this.tabRevMap, + * aber rückwärts funktioniert nicht.. :( + * + * Weil: "shift+tab" triggert keypress event nicht. + * -> nur "tab" (keyCodwe == 9) triggert keypress event. + * + * keydown wird durch "shift" getriggert, aber nicht durch "tab"?! + * -> während shift gedrückt ist kommt auch kein keypress event von tab. WARUM?! + * + * Ist das irgendwo im code deaktiviert oder ist das hier eine ALTE VERSION? + * @param {HTMLElement} div + * @param {HTMLElement} prevDiv + * @param {HTMLElement} nextDiv + */ + let initTabCallback = (div, prevDiv, nextDiv) => { + //this.tabRevMap.set(div, prevDiv); + this._tabFwdMap.set(div, nextDiv); + + div.onkeypress = (e) => { + if (e.keyCode == 9) { + e.preventDefault(); + // let nextElement = null; + // if(e.shiftKey) { + // nextElement = this.tabRevMap.get(document.activeElement) + // } else { + let nextElement = this._tabFwdMap.get(document.activeElement) + //} + if (nextElement != null) { + nextElement.focus(); + } + } + }; + } + + initTabCallback(this._nameDiv, this._rotationWDiv, this._anchorDiv); + initTabCallback(this._anchorDiv, this._nameDiv, this._locationLngDiv); + initTabCallback(this._locationLngDiv, this._anchorDiv, this._locationLatDiv); + initTabCallback(this._locationLatDiv, this._locationLngDiv, this._elevationDiv); + initTabCallback(this._elevationDiv, this._locationLatDiv, this._scaleDiv); + initTabCallback(this._scaleDiv, this._elevationDiv, this._sizeDiv); + initTabCallback(this._sizeDiv, this._scaleDiv, this._rotationXDiv); + initTabCallback(this._rotationXDiv, this._scaleDiv, this._rotationYDiv); + initTabCallback(this._rotationYDiv, this._rotationXDiv, this._rotationZDiv); + initTabCallback(this._rotationZDiv, this._rotationYDiv, this._rotationWDiv); + initTabCallback(this._rotationWDiv, this._rotationZDiv, this._nameDiv); + + document.querySelector("#simple-objects-editor-anchor + div > button").onclick = () => { + this._anchorDiv.value = CosmoScout.state.activePlanetCenter; + }; + + document.querySelector("#simple-objects-editor-rotation-w + div > button").onclick = () => { + this._rotationXDiv.value = CosmoScout.state.observerRotation[0]; + this._rotationYDiv.value = CosmoScout.state.observerRotation[1]; + this._rotationZDiv.value = CosmoScout.state.observerRotation[2]; + this._rotationWDiv.value = CosmoScout.state.observerRotation[3]; + }; + + document.querySelector("#simple-objects-editor-pick-location").onclick = () => { + this.setPickLocationEnabled(!this._pickLocationButtonActive); + }; + + // close button ------------------------------------------------------------------------------- + document.querySelector("#simple-objects-editor-close").onclick = () => { + if(this._nameDiv.value != null) { + CosmoScout.callbacks.simpleObjects.undoEdit(this._nameDiv.value); + } + this._closeEditor(); + }; + + // Delete bookmarks on delete button click ----------------------------------------------------- + this._deleteButton.onclick = () => { + if (this._editObjectName != null && this.hasName(this._editObjectName)) { + this.remove(this._editObjectName); + this._editObjectName = null; + } + + this._closeEditor(); + }; + + // Save bookmark on save button click ---------------------------------------------------------- + this._saveButton.onclick = () => { + + if(!this.validateInput()) return; + + CosmoScout.callbacks.simpleObjects.save(this._editObjectName == null ? "" : this._editObjectName, + this._nameDiv.value, JSON.stringify(this.generateJSONObject())); + this._closeEditor(); + }; + + /** + * Adds a event listener to the given element, that invokes a timer after + * @param {HTMLElement} div + * @param {string} eventType + * @param {number} tdelay + */ + let initUpdateOnChange = (div, eventType = "keypress", tdelay = 750) => { + let timeout = null; + + div.addEventListener(eventType, (e) => { + clearTimeout(timeout); + timeout = setTimeout(() => { + if(this.validateInput(true)) { this.updateModel(); } + }, tdelay); + }); + } + + initUpdateOnChange(this._nameDiv); + initUpdateOnChange(this._anchorDiv); + initUpdateOnChange(this._modelSelect, "change"); + initUpdateOnChange(this._mapSelect, "change"); + initUpdateOnChange(this._locationLngDiv, "change"); + initUpdateOnChange(this._locationLatDiv, "change"); + initUpdateOnChange(this._locationLngDiv); + initUpdateOnChange(this._locationLatDiv); + initUpdateOnChange(this._elevationDiv); + initUpdateOnChange(this._scaleDiv); + initUpdateOnChange(this._sizeDiv); + initUpdateOnChange(this._rotationXDiv); + initUpdateOnChange(this._rotationYDiv); + initUpdateOnChange(this._rotationZDiv); + initUpdateOnChange(this._rotationWDiv); + initUpdateOnChange(this._alignSurfaceDiv, "change", 0); + + initUpdateOnChange(this._nameDiv, "keyup"); + initUpdateOnChange(this._anchorDiv, "keyup"); + initUpdateOnChange(this._locationLngDiv, "keyup"); + initUpdateOnChange(this._locationLatDiv, "keyup"); + initUpdateOnChange(this._elevationDiv, "keyup"); + initUpdateOnChange(this._scaleDiv, "keyup"); + initUpdateOnChange(this._sizeDiv, "keyup"); + initUpdateOnChange(this._rotationXDiv, "keyup"); + initUpdateOnChange(this._rotationYDiv, "keyup"); + initUpdateOnChange(this._rotationZDiv, "keyup"); + initUpdateOnChange(this._rotationWDiv, "keyup"); + } + + +// --------------- basic functions used by the GUI -------------- + + /** + * Opens the editor and clears all fields to create a new object. + */ + add() { + this._resetFields(); + this._resetInvalid(); + this._deleteButton.classList.add("hidden"); + this._editObjectName = null; + + this._openEditor("Add new model object"); + this._nameDiv.focus(); + } + + /** + * Opens the editor and parses the json parameters into their related fields. + * It is called from the backend after the user clicked an edit button and + * the backend found the json config of the object. + * @param {string} objectName + * @param {string} json + */ + edit(objectName, json) { + this._resetFields(); + this._resetInvalid(); + + this._deleteButton.classList.remove("hidden"); + + let object = JSON.parse(json); + + this._editObjectName = objectName; + this._nameDiv.value = objectName; + + let modelFile = object.modelFile.replace("../share/resources/models/",""); + let environmentMap = object.environmentMap.replace("../share/resources/textures/",""); + + this._modelSelect.value = modelFile; + this._mapSelect.value = environmentMap; + this._modelButton.firstChild.firstChild.firstChild.innerHTML = modelFile; + this._mapButton.firstChild.firstChild.firstChild.innerHTML = environmentMap; + + this._anchorDiv.value = object.anchor; + this._locationLngDiv.value = object.lngLat[0]; + this._locationLatDiv.value = object.lngLat[1]; + + if(object.elevation) { this._elevationDiv.value = object.elevation; } + if(object.scale) { this._scaleDiv.value = object.scale; } + if(object.diagonalLength) { this._sizeDiv.value = object.diagonalLength; } + + if(object.rotation) { + this._rotationXDiv.value = object.rotation[0]; + this._rotationYDiv.value = object.rotation[1]; + this._rotationZDiv.value = object.rotation[2]; + this._rotationWDiv.value = object.rotation[3]; + } + + if(object.alignToSurface) { + this._alignSurfaceDiv.checked = object.alignToSurface; + } + + this._openEditor("Edit object"); + } + + + /** + * Removes the given object from the list and calls the backend remove function + * to erase the object (with the same name) and temporary object from the scene graph. + * @param {string} objectName + */ + remove(objectName) { + this.removeObjectFromList(objectName); + CosmoScout.callbacks.simpleObjects.remove(this._editObjectName); + } + + + /** + * Sends the current object configuration to the backend wich updates the temporary displayed object. + * @returns If at least one mandatory parameter is not provided. + */ + updateModel() { + let nameGiven = this._nameDiv.value != ""; + let anchorGiven = this._anchorDiv.value != ""; + let modelGiven = !(this._modelSelect.value == "-1" || this._modelSelect.value == ""); + let mapGiven = !(this._mapSelect.value == "-1" || this._mapSelect.value == ""); + let locationGiven = this._locationLngDiv.value != "" && this._locationLngDiv.value != ""; + + if(!(nameGiven && anchorGiven && modelGiven && mapGiven && locationGiven)) { + return; + } + + CosmoScout.callbacks.simpleObjects.update(this._nameDiv.value, JSON.stringify(this.generateJSONObject())); + + //console.debug("Updated Model"); + } + +// ------------ Object list functions --------------------- + + /** + * Adds a new item to the list of objects. + * @param {string} objectName Unformatted name of the object + * @returns If the object already exists + */ + addObjectToList(objectName) { + + if(this.hasName(objectName)) { + console.error("The object \"" + objectName + "\" already exists."); + return; + } + + this.addName(objectName); + + let listItem = CosmoScout.gui.loadTemplateContent('simple-objects-list-item'); + listItem.innerHTML = listItem.innerHTML.replace(/%NAME%/g, objectName).replace(/%ID%/g, objectName).trim(); + listItem.id = `simple-object-id-${this.formatName(objectName)}`; + + document.getElementById('simple-objects-list').appendChild(listItem); + + CosmoScout.gui.initTooltips(); + + this._sortObjectList(document.getElementById('simple-objects-list')); + } + + /** + * Removes the corresponding entry from the list of objects + * @param {string} objectName Unformatted name of the object + * @returns If the object does not exist + */ + removeObjectFromList(objectName) { + + if(!this.hasName(objectName)) { + console.error("The object \"" + objectName + "\" does not exist."); + return; + } + + this.removeName(objectName) + let object = document.querySelector("#simple-object-id-" + this.formatName(objectName)); + if (object) { + object.remove(); + } + } + +// ------------ Helper functions for registered names --------------------- + + /** + * Return true if the object name is registered + * @param {string} objectName (can be unformatted) + * @returns {Boolean} + */ + hasName(objectName) { + return this._objectNames.has(this.formatName(objectName)); + } + + /** + * Adds the name to the set of known objects names + * @param {string} objectName + */ + addName(objectName) { + this._objectNames.add(this.formatName(objectName)); + } + + /** + * Removes the name from the set of known objects names + * @param {string} objectName + */ + removeName(objectName) { + this._objectNames.delete(this.formatName(objectName)); + } + + /** + * Formats the object name. This removes all whitespaces. + * @param {string} objectName + * @returns object name without whitespaces + */ + formatName(objectName) { + return objectName.replace(/\s/g,''); + } + + +// ------------ Input validation ---------------------------------------- + + /** + * Validates all input fields. Checks are made in following order: + * + * (*) only executed if checkFormatOnly is false + * + * - are required fields given? (*) + * - whether name is still unused + * - location inputs -> all or none + * - rotation inputs -> all or none + * - name format (\"<> symbols are not allowed) + * - format of all numbers + * + * @param {boolean} checkFormatOnly + * @returns {boolean} returns true if all fields are valid. + */ + validateInput(checkFormatOnly = false) { + + this._resetInvalid(); + + let allCorrect = true; + + let markInvalid = (div, isInvalid, message) => { + if (isInvalid) { + div.classList.add("is-invalid"); + div.parentNode.querySelector(".invalid-feedback").textContent = message; + allCorrect = false; + } else { + div.classList.remove("is-invalid"); + } + }; + + let nameGiven = this._nameDiv.value != ""; + let anyLocationGiven = this._locationLngDiv.value != "" || this._locationLngDiv.value != ""; + let anyRotationGiven = this._rotationXDiv.value != "" || this._rotationYDiv.value != "" + || this._rotationZDiv.value != "" || this._rotationWDiv.value != ""; + + // Check for completeness first, then for correct formatting. + if(!checkFormatOnly) { + + let anchorGiven = this._anchorDiv.value != ""; + let modelGiven = !(this._modelSelect.value == "-1" || this._modelSelect.value == ""); + let mapGiven = !(this._mapSelect.value == "-1" || this._mapSelect.value == ""); + + let locationGiven = this._locationLngDiv.value != "" && this._locationLngDiv.value != ""; + + + if (!(nameGiven || anchorGiven || locationGiven || mapGiven || modelGiven)) { + + let markInvalid = (div) => { + div.classList.add("is-invalid"); + div.parentNode.querySelector(".invalid-feedback").textContent = ""; + } + + markInvalid(this._nameDiv); + markInvalid(this._anchorDiv); + markInvalid(this._mapButton); + markInvalid(this._modelButton); + markInvalid(this._locationLngDiv); + markInvalid(this._locationLatDiv); + + this._nothingGivenError.style.display = "block"; + allCorrect = false; + + } else { + + markInvalid(this._nameDiv, !nameGiven, "Please choose a name."); + markInvalid(this._anchorDiv, !anchorGiven, "Please choose an anchor."); + markInvalid(this._mapButton, !mapGiven, "Please select a map."); + markInvalid(this._modelButton, !modelGiven, "Please select a model."); + + if (!anyLocationGiven) { + markInvalid(this._locationLatDiv, true, ""); + markInvalid(this._locationLngDiv, true, "Set a location."); + } + + this._nothingGivenError.style.display = "none"; + } + } + + // Now CHECK FORMATTING of the given values. + + // Print error if an object with given name already exists + if(nameGiven) { + markInvalid(this._nameDiv, + this.hasName(this._nameDiv.value) && (this._editObjectName == null || this._editObjectName != this._nameDiv.value), + "This name is already in use."); + } + + if(anyLocationGiven) { + markInvalid(this._locationLngDiv, this._locationLngDiv.value == "", "Both values are required."); + markInvalid(this._locationLatDiv, this._locationLatDiv.value == "", "Both values are required."); + } + + if(anyRotationGiven) { + let errorMsg = "Provide all rotation values or none."; + markInvalid(this._rotationXDiv, this._rotationXDiv.value == "", errorMsg); + markInvalid(this._rotationYDiv, this._rotationYDiv.value == "", errorMsg); + markInvalid(this._rotationZDiv, this._rotationZDiv.value == "", errorMsg); + markInvalid(this._rotationWDiv, this._rotationWDiv.value == "", errorMsg); + } + + // Abort if errors occurred. + if(!allCorrect) return false; + + var textRegex = /[\\"<>]/; + let textError = "This shouldn't contain special characters like \\, \", < or >."; + markInvalid(this._nameDiv, this._nameDiv.value.match(textRegex), textError); + + let numRegex = /^[-+]?[0-9]+(\.[0-9]*)?$/; + let numError = "Must be a number."; + + if(anyLocationGiven) { + markInvalid(this._locationLngDiv, !this._locationLngDiv.value.match(numRegex), numError); + markInvalid(this._locationLatDiv, !this._locationLatDiv.value.match(numRegex), numError); + } + + if(this._elevationDiv.value) { + markInvalid(this._elevationDiv, !this._elevationDiv.value.match(numRegex), numError); + } + if(this._scaleDiv.value) { + markInvalid(this._scaleDiv, !this._scaleDiv.value.match(numRegex), numError); + } + if(this._sizeDiv.value) { + markInvalid(this._sizeDiv, !this._sizeDiv.value.match(numRegex), numError); + } + if (anyRotationGiven) { + markInvalid(this._rotationXDiv, !this._rotationXDiv.value.match(numRegex), numError); + markInvalid(this._rotationYDiv, !this._rotationYDiv.value.match(numRegex), numError); + markInvalid(this._rotationZDiv, !this._rotationZDiv.value.match(numRegex), numError); + markInvalid(this._rotationWDiv, !this._rotationWDiv.value.match(numRegex), numError); + } + + return allCorrect; + } + + + /** + * Generates a JSON object holding the given configuration oth the simple object. + * @param {boolean} deleteFromList idk, why I implemented this. Maybe it can be deleted. + * @returns {object} JSON object holding the object configuration + */ + generateJSONObject(deleteFromList = true) { + let object = { + modelFile: "../share/resources/models/" + this._modelSelect.value, + environmentMap: "../share/resources/textures/" + this._mapSelect.value, + anchor: this._anchorDiv.value, + lngLat: [ + parseFloat(this._locationLngDiv.value), + parseFloat(this._locationLatDiv.value), + ] + }; + + if(this._elevationDiv.value) { + object.elevation = parseFloat(this._elevationDiv.value); + } + + if(this._scaleDiv.value) { + object.scale = parseFloat(this._scaleDiv.value); + } + + if(this._sizeDiv.value) { + object.diagonalLength = parseFloat(this._sizeDiv.value); + } + + if (this._rotationXDiv.value || this._rotationYDiv.value || this._rotationZDiv.value || this._rotationWDiv.value) { + object.rotation = [ + parseFloat(this._rotationXDiv.value), parseFloat(this._rotationYDiv.value), + parseFloat(this._rotationZDiv.value), parseFloat(this._rotationWDiv.value) + ]; + } + + if(this._alignSurfaceDiv.checked) { + object.alignToSurface = true; + } + + return object; + } + + +// ------------ Functions for picking a location on the ground ----------- + + /** + * Toggles the availability of the pick location functionality automatically based on the height of the observer. + * Only if the observer is closer to the ground than minHeight, the button is clickable. + * @returns If the editor is not open + */ + updatePickLocationButton() { + if (!this._editorOpen) return; + + let pos = CosmoScout.state.observerLngLatHeight; + if (pos !== undefined) { + + const minHeight = 1500; + + if(pos[2] <= minHeight) { + this._pickLocationButton.classList.remove("disabled"); + this._lastHeight = pos[2]; + } else if (this._lastHeight <= minHeight) { + this._pickLocationButton.classList.add("disabled"); + this.setPickLocationEnabled(false); + this._lastHeight = pos[2]; + } + } + } + + /** + * Enables/disables the pick location functionality in the backend and the button in the editor. + * @param {boolean} enable + */ + setPickLocationEnabled(enable) { + + this._pickLocationButtonActive = enable; + CosmoScout.callbacks.simpleObjects.setPickLocationEnabled(enable); + + if(!enable) { + this._pickLocationButton.classList.remove("active"); + } + } + + /** + * Sets the location, anchor name and disables the pick location button. + * Is usually called from the plugin backend after the location was picked. + * @param {number|string} lng Longitude coordinate + * @param {number|string} lat Latitude coordinate + * @param {string} anchor Anchor name + */ + setLngLatAnchor(lng, lat, anchor) { + this._locationLngDiv.value = lng; + this._locationLatDiv.value = lat; + this._anchorDiv.value = anchor; + this.setPickLocationEnabled(false); + + if(this.validateInput(true)) { this.updateModel(); } + } + + + + +// ------- Private utility funcitons for the editor window ----------- + + /** + * Displays the editor window, sets the title and disables the pick location button. + * @param {string} title + * @private + */ + _openEditor(title) { + this._title.textContent = title; + this._editor.classList.add("visible"); + this._pickLocationButton.classList.add("disabled"); + this._editorOpen = true; + this.setPickLocationEnabled(false); + } + + /** + * Hides the editor window + * @private + */ + _closeEditor() { + this._editor.classList.remove("visible"); + this._editorOpen = false; + } + + /** + * Clears all input fields + * @private + */ + _resetFields() { + this._nameDiv.value = ""; + this._anchorDiv.value = ""; + this._modelSelect.value = "-1"; + this._mapSelect.value = "-1"; + this._modelButton.firstChild.firstChild.firstChild.innerHTML = this._modelSelect.options[this._modelSelect.selectedIndex].text; + this._mapButton.firstChild.firstChild.firstChild.innerHTML = this._mapSelect.options[this._mapSelect.selectedIndex].text; + this._locationLngDiv.value = ""; + this._locationLatDiv.value = ""; + this._elevationDiv.value = ""; + this._scaleDiv.value = ""; + this._sizeDiv.value = ""; + this._rotationXDiv.value = ""; + this._rotationYDiv.value = ""; + this._rotationZDiv.value = ""; + this._rotationWDiv.value = ""; + this._alignSurfaceDiv.checked = false; + CosmoScout.callbacks.simpleObjects.setAlignToSufaceEnabled(false); + } + + /** + * Resets all invalid markers of the input fields. + * @private + */ + _resetInvalid() { + let removeFrom = (div) => { + div.classList.remove("is-invalid"); + div.parentNode.querySelector(".invalid-feedback").textContent = ""; + } + + removeFrom(this._nameDiv); + removeFrom(this._anchorDiv); + removeFrom(this._modelButton); + removeFrom(this._mapButton); + removeFrom(this._locationLngDiv); + removeFrom(this._locationLatDiv); + removeFrom(this._elevationDiv); + removeFrom(this._scaleDiv); + removeFrom(this._sizeDiv); + removeFrom(this._rotationXDiv); + removeFrom(this._rotationYDiv); + removeFrom(this._rotationZDiv); + removeFrom(this._rotationWDiv); + + this._nothingGivenError.style.display = "none"; + } + + /** + * Sorts the list of simple objects alphabetically. + * @param {HTMLElement} container The element containing simpleobject divs. + * @private + */ + _sortObjectList(container) { + Array.prototype.slice.call(container.children) + .sort((ea, eb) => { + let a = ea.querySelector(".simpleobjects-name").textContent; + let b = eb.querySelector(".simpleobjects-name").textContent; + return a < b ? -1 : (a > b ? 1 : 0); + }) + .forEach((div) => { + container.appendChild(div); + }); + } + } + + CosmoScout.init(SimpleObjectsEditorApi); +})(); \ No newline at end of file diff --git a/plugins/csp-simple-objects/gui/simple-objects-editor.html b/plugins/csp-simple-objects/gui/simple-objects-editor.html new file mode 100644 index 000000000..bd8df8419 --- /dev/null +++ b/plugins/csp-simple-objects/gui/simple-objects-editor.html @@ -0,0 +1,146 @@ +
+
+ +
+
+ edit + 3D Model Editor +
+ + close + +
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+
+
+
+ +
+
+
+ Please fill all required fields. +
+ +
+
+ + +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ + + + +
+ +
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/plugins/csp-simple-objects/gui/simple-objects-tab.html b/plugins/csp-simple-objects/gui/simple-objects-tab.html new file mode 100644 index 000000000..6abbe33c9 --- /dev/null +++ b/plugins/csp-simple-objects/gui/simple-objects-tab.html @@ -0,0 +1,13 @@ +
+
+
+
+ + \ No newline at end of file diff --git a/plugins/csp-simple-objects/gui/simple-objects-templates.html b/plugins/csp-simple-objects/gui/simple-objects-templates.html new file mode 100644 index 000000000..b1b08b3f3 --- /dev/null +++ b/plugins/csp-simple-objects/gui/simple-objects-templates.html @@ -0,0 +1,20 @@ + diff --git a/plugins/csp-simple-objects/src/Plugin.cpp b/plugins/csp-simple-objects/src/Plugin.cpp new file mode 100644 index 000000000..25970d8a9 --- /dev/null +++ b/plugins/csp-simple-objects/src/Plugin.cpp @@ -0,0 +1,410 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "Plugin.hpp" + +#include "SimpleObject.hpp" +#include "logger.hpp" + +#include "../../../src/cs-core/InputManager.hpp" +#include "../../../src/cs-core/GuiManager.hpp" +#include "../../../src/cs-core/SolarSystem.hpp" +#include "../../../src/cs-utils/convert.hpp" +#include "../../../src/cs-utils/logger.hpp" +#include "../../../src/cs-utils/filesystem.hpp" + +#include + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +EXPORT_FN cs::core::PluginBase* create() { + return new csp::simpleobjects::Plugin; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +EXPORT_FN void destroy(cs::core::PluginBase* pluginBase) { + delete pluginBase; // NOLINT(cppcoreguidelines-owning-memory) +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace csp::simpleobjects { + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void from_json(nlohmann::json const& j, Plugin::Settings::SimpleObject& o) { + cs::core::Settings::deserialize(j, "modelFile", o.mModelFile); + cs::core::Settings::deserialize(j, "environmentMap", o.mEnvironmentMap); + cs::core::Settings::deserialize(j, "anchor", o.mAnchorName); + cs::core::Settings::deserialize(j, "lngLat", o.mLngLat); + cs::core::Settings::deserialize(j, "rotation", o.mRotation); + cs::core::Settings::deserialize(j, "alignToSurface", o.mAlignToSurface); + cs::core::Settings::deserialize(j, "elevation", o.mElevation); + cs::core::Settings::deserialize(j, "scale", o.mScale); + cs::core::Settings::deserialize(j, "diagonalLength", o.mDiagonalLength); +} + +void to_json(nlohmann::json& j, Plugin::Settings::SimpleObject const& o) { + cs::core::Settings::serialize(j, "modelFile", o.mModelFile); + cs::core::Settings::serialize(j, "environmentMap", o.mEnvironmentMap); + cs::core::Settings::serialize(j, "anchor", o.mAnchorName); + cs::core::Settings::serialize(j, "lngLat", o.mLngLat); + cs::core::Settings::serialize(j, "rotation", o.mRotation); + cs::core::Settings::serialize(j, "alignToSurface", o.mAlignToSurface); + cs::core::Settings::serialize(j, "elevation", o.mElevation); + cs::core::Settings::serialize(j, "scale", o.mScale); + cs::core::Settings::serialize(j, "diagonalLength", o.mDiagonalLength); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void from_json(nlohmann::json const& j, Plugin::Settings& o) { + cs::core::Settings::deserialize(j, "objects", o.mSimpleObjects); +} + +void to_json(nlohmann::json& j, Plugin::Settings const& o) { + cs::core::Settings::serialize(j, "objects", o.mSimpleObjects); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void Plugin::init() { + + logger().info("Loading plugin..."); + +// Initializing GUI elements + + mGuiManager->addCssToGui("css/csp-simple-objects.css"); + mGuiManager->addHtmlToGui("simple-objects-editor", "../share/resources/gui/simple-objects-editor.html"); + mGuiManager->addHtmlToGui("simple-objects-list-item-template", "../share/resources/gui/simple-objects-templates.html"); + mGuiManager->addScriptToGuiFromJS("../share/resources/gui/js/csp-simple-objects-editor.js"); + mGuiManager->addPluginTabToSideBarFromHTML("Objects", "view_in_ar", "../share/resources/gui/simple-objects-tab.html"); + + +// -------- Initializing GUI callback functions ------------------------ + + /** + * This function is called when a edit button in the object list is clicked. + * It sends the corresponding json config to the gui which loads it into the editor + * and creates a new temporary object which displays the changes entered in the editor. + * The object that is being edited is hidden until the changes are saved / discarded. + */ + + mGuiManager->getGui()->registerCallback("simpleObjects.edit", + "Edits the settings of the simple object with given ID.", + std::function([this](std::string&& objectName) { + + logger().debug("Edit called for object '{}'", objectName); + + auto object = mPluginSettings.mSimpleObjects.find(objectName); + if (object != mPluginSettings.mSimpleObjects.end()) { + nlohmann::json json = object->second; + mGuiManager->getGui()->callJavascript("CosmoScout.simpleObjectsEditor.edit", objectName, json.dump()); + + auto it = std::find_if(mSimpleObjects.begin(), mSimpleObjects.end(), + [&objectName](const std::shared_ptr& obj) { + return obj->getName() == objectName; + }); + + if (it != mSimpleObjects.end()) { + tmpSimpleObject = std::make_shared(objectName, object->second, mSceneGraph, mAllSettings, mSolarSystem); + tmpSimpleObject->setSun(mSolarSystem->getSun()); + tmpSimpleObject->update(); + + (*it)->setEditEnabled(true); + } + + } else { + logger().warn("Failed to execute 'simpleObjects.edit' for object '{}': No such object registered!", objectName); + } + })); + + + /** + * The update callback is called as soon as some changes in the editor are made. + * It will display them by writing the new configuration to the temporary object. + * If the temporary object does not exist, it will be created with the new configuration. + */ + + mGuiManager->getGui()->registerCallback("simpleObjects.update", + "Generates a new temporary simple object to display if it does not exist yet or updates the settings otherwise.", + std::function([this](std::string&& name, std::string&& jsonString) { + + Settings::SimpleObject settings; + auto json = nlohmann::json::parse(jsonString); + json.get_to(settings); + + if(tmpSimpleObject == nullptr) { + auto simpleobject = std::make_shared(name, settings, mSceneGraph, mAllSettings, mSolarSystem); + simpleobject->setSun(mSolarSystem->getSun()); + tmpSimpleObject = std::move(simpleobject); + } else { + tmpSimpleObject->updateConfig(name, settings); + } + })); + + + /** + * Saves the configuration to the object with the given name 'newName' and deletes the temporary object. + * If the name didn't change, the configuration of the object will be updated accordingly. + * Otherwise the old one will be deleted and an object with the new name and new config will be created. + */ + + mGuiManager->getGui()->registerCallback("simpleObjects.save", + "Adds a new simple object to the scene. If a temporary object with the same name exists, it will be deleted.", + std::function([this](std::string&& oldName, std::string&& newName, std::string&& jsonString) { + + logger().debug("Save called. Old Name: '{}' New Name: '{}'", oldName, newName); + + Settings::SimpleObject settings; + auto json = nlohmann::json::parse(jsonString); + json.get_to(settings); + + if(oldName == newName) { + mPluginSettings.mSimpleObjects[oldName] = settings; + + auto it = std::find_if(mSimpleObjects.begin(), mSimpleObjects.end(), + [&oldName](const std::shared_ptr& obj) { + return obj->getName() == oldName; + }); + + (*it)->updateConfig(oldName, settings); + (*it)->setEditEnabled(false); + + } else { + + addObject(newName, settings); + mPluginSettings.mSimpleObjects[newName] = settings; + + if(oldName != "") { + mSimpleObjects.erase( + std::remove_if( + mSimpleObjects.begin(), + mSimpleObjects.end(), + [&](std::shared_ptr const & so) { return so->getName() == oldName; } + ), + mSimpleObjects.end() + ); + + mPluginSettings.mSimpleObjects.erase(oldName); + mGuiManager->getGui()->callJavascript("CosmoScout.simpleObjectsEditor.removeObjectFromList", oldName); + } + + } + + tmpSimpleObject.reset(); + })); + + + /** + * This function is called when the close button of the editor is clicked. + * In this case no changes should be made, so the temporary object just needs to be removed and + * the normal object without any changes will be displayed again. + */ + + mGuiManager->getGui()->registerCallback("simpleObjects.undoEdit", + "Reverts all changes of the edited object with given name. (Disables the edit mode and deletes the temporary object.)", + std::function([this](std::string&& objectName) { + + logger().debug("UndoEdit called for object '{}'", objectName); + + auto it = std::find_if(mSimpleObjects.begin(), mSimpleObjects.end(), + [&objectName](const std::shared_ptr& obj) { + return obj->getName() == objectName; + }); + + if (it != mSimpleObjects.end()) { + (*it)->setEditEnabled(false); + } + + tmpSimpleObject.reset(); + })); + + + /** + * The remove callback function deletes the object with given name und clears the temporary object. + */ + + mGuiManager->getGui()->registerCallback("simpleObjects.remove", + "Removes the simple object with the given ID.", + std::function([this](std::string&& objectName) { + + logger().debug("Remove called for object '{}'", objectName); + + mPluginSettings.mSimpleObjects.erase(objectName); + mSimpleObjects.erase( + std::remove_if( + mSimpleObjects.begin(), + mSimpleObjects.end(), + [&](std::shared_ptr const & so) { return so->getName() == objectName; } + ), + mSimpleObjects.end() + ); + tmpSimpleObject.reset(); + })); + + + mGuiManager->getGui()->registerCallback("simpleObjects.setPickLocationEnabled", + "Toggles the ability to pick a location on the ground that will be set in the simple objects editor", + std::function([this](bool enable) { pickLocationToolEnabled = enable; })); + + + + // TODO: the next 3 callback functions are not used. + // Removing them does not work because the gui needs them as default callback functions?! + // Did not search for a way to remove callback entry from the button divs yet. + + mGuiManager->getGui()->registerCallback("simpleObjects.setAlignToSufaceEnabled", + "Sets whether the current object should be aligned to the surface.", + std::function([this](bool enable) { alignToSurfaceEnabled = enable; })); + + mGuiManager->getGui()->registerCallback("simpleObjects.setModelFile", + "Sets the file name of the gltf model file.", + std::function([this](std::string&& model) { modelFile = model; })); + + mGuiManager->getGui()->registerCallback("simpleObjects.setEnvironmentMap", + "Sets the file name of the environment map.", + std::function([this](std::string&& map) { environmentMap = map; })); + + + + + /** + * When the pick location functionality is enabled, this function checks for an intersection with a CelestialBody + * in the free FOV where no GUI elements are. + * If available, the coordinates at the clicked location and the SPICE center are send to the GUI. + */ + + mOnClickConnection = mInputManager->pButtons[0].connect([this](bool pressed) { + if (!pressed && !mInputManager->pHoveredGuiItem.get() && pickLocationToolEnabled) { + auto intersection = mInputManager->pHoveredObject.get().mObject; + + if (!intersection) { + return; + } + + auto body = std::dynamic_pointer_cast(intersection); + + if (!body) { + return; + } + + auto radii = body->getRadii(); + auto lngLat = cs::utils::convert::toDegrees(cs::utils::convert::cartesianToLngLat(mInputManager->pHoveredObject.get().mPosition, radii)); + mGuiManager->getGui()->callJavascript("CosmoScout.simpleObjectsEditor.setLngLatAnchor", lngLat[0], lngLat[1], body->getCenterName()); + } + }); + + +// Find model and environment map files and add them to the dropdown menus. + + logger().info("Scanning for model files.."); + initDropdown("simpleObjects.setModelFile", "../share/resources/models", std::regex(".+(\\.gltf|\\.glb)$")); + + logger().info("Scanning for environment maps.."); + initDropdown("simpleObjects.setEnvironmentMap", "../share/resources/textures", std::regex(".+(\\.dds)$")); + + +// Load and place all objects from the settings.json + + + mPluginSettings = mAllSettings->mPlugins.at("csp-simple-objects"); + + if(mPluginSettings.mSimpleObjects.size() > 0) { + logger().info("Loading objects from settings.."); + } else { + logger().warn("No objects configured in the settings."); + } + + for (auto const& objSettings : mPluginSettings.mSimpleObjects) { + + logger().debug(" * {}", objSettings.first); + + // check whether the specified anchor exists + auto anchor = mAllSettings->mAnchors.find(objSettings.second.mAnchorName); + if (anchor == mAllSettings->mAnchors.end()) { + throw std::runtime_error( + "There is no Anchor \"" + objSettings.first + "\" defined in the settings."); + } + addObject(objSettings.first, objSettings.second); + } + + logger().info("Loading done."); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void Plugin::deInit() { + logger().info("Unloading plugin..."); + + mGuiManager->removePluginTab("Objects"); + + mGuiManager->getGui()->unregisterCallback("simpleObjects.setPickLocationEnabled"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.setAlignToSufaceEnabled"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.setModelFile"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.setEnvironmentMap"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.edit"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.update"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.save"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.undoEdit"); + mGuiManager->getGui()->unregisterCallback("simpleObjects.remove"); + + mGuiManager->getGui()->callJavascript("CosmoScout.gui.unregisterHtml", "simple-objects-editor"); + mGuiManager->getGui()->callJavascript("CosmoScout.gui.unregisterHtml", "simple-objects-list-item-template"); + mGuiManager->getGui()->callJavascript("CosmoScout.gui.unregisterCss", "css/csp-simple-objects.css"); + + mInputManager->pButtons[0].disconnect(mOnClickConnection); + + logger().info("Unloading done."); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void Plugin::update() { + for(std::shared_ptr so : mSimpleObjects) { + so->update(); + } + + if(tmpSimpleObject != nullptr) { + tmpSimpleObject->update(); + } + + mGuiManager->getGui()->callJavascript("CosmoScout.simpleObjectsEditor.updatePickLocationButton"); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void Plugin::initDropdown(const std::string jsFunction, const std::string folder, const std::regex pattern) { + auto files(cs::utils::filesystem::listFiles(folder, pattern)); + + for (auto const& file : files) { + std::string name(file); + logger().debug(" * {}", name); + + const size_t lastSlashIdx = name.find_last_of("\\/"); + if (std::string::npos != lastSlashIdx) { + name.erase(0, lastSlashIdx + 1); + } + + mGuiManager->getGui()->callJavascript( + "CosmoScout.gui.addDropdownValue", jsFunction, name, name, false); + } +} + +void Plugin::addObject(std::string name, Settings::SimpleObject settings) { + + auto simpleobject = std::make_shared(name, settings, mSceneGraph, mAllSettings, mSolarSystem); + + simpleobject->setSun(mSolarSystem->getSun()); + + mGuiManager->getGui()->callJavascript("CosmoScout.simpleObjectsEditor.addObjectToList", name); + + mSimpleObjects.push_back(simpleobject); +} + +} // namespace csp::satellites \ No newline at end of file diff --git a/plugins/csp-simple-objects/src/Plugin.hpp b/plugins/csp-simple-objects/src/Plugin.hpp new file mode 100644 index 000000000..0d87be03c --- /dev/null +++ b/plugins/csp-simple-objects/src/Plugin.hpp @@ -0,0 +1,76 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CSP_SIMPLE_OBJECTS_PLUGIN_HPP +#define CSP_SIMPLE_OBJECTS_PLUGIN_HPP + +#include "../../../src/cs-core/PluginBase.hpp" +#include "../../../src/cs-utils/DefaultProperty.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace csp::simpleobjects { + +class SimpleObject; + +/// This plugin enables to place simpleobjects on celestial bodies in the Solar System. +/// The configuration of this plugin is done via the provided json config. See README.md for +/// details. +class Plugin : public cs::core::PluginBase { + public: + struct Settings { + + /// The settings for a simpleobject. + struct SimpleObject { + /// Path to the model. ".glb" and ".gltf" are allowed formats. + std::string mModelFile; + /// Path to the environment map. ".dds", ".ktx" and ".kmg" are allowed formats. + std::string mEnvironmentMap; + std::string mAnchorName; + glm::dvec2 mLngLat; + cs::utils::DefaultProperty mRotation{glm::dquat(1.0,0.0,0.0,0.0)}; + cs::utils::DefaultProperty mAlignToSurface{false}; + cs::utils::DefaultProperty mElevation{0.0}; + cs::utils::DefaultProperty mScale{1.0}; + cs::utils::DefaultProperty mDiagonalLength{5.0}; + }; + + std::map mSimpleObjects; + }; + + void init() override; + void deInit() override; + void update() override; + + + private: + Settings mPluginSettings; + std::vector> mSimpleObjects; + std::shared_ptr tmpSimpleObject; + std::shared_ptr minVisibilityAngle; + + bool pickLocationToolEnabled = false; + int mOnClickConnection = -1; + int mOnObjectAddedConnection = -1; + int mOnObjectRemovedConnection = -1; + + std::string modelFile = ""; + std::string environmentMap = ""; + bool alignToSurfaceEnabled = false; + + void initDropdown(const std::string callbackName, const std::string folder, std::regex pattern); + void addObject(std::string name, Settings::SimpleObject settings); +}; + +} // namespace csp::simpleobjects + +#endif // CSP_SIMPLE_OBJECTS_PLUGIN_HPP diff --git a/plugins/csp-simple-objects/src/SimpleObject.cpp b/plugins/csp-simple-objects/src/SimpleObject.cpp new file mode 100644 index 000000000..6aa45d52c --- /dev/null +++ b/plugins/csp-simple-objects/src/SimpleObject.cpp @@ -0,0 +1,295 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "SimpleObject.hpp" + +#include +#include +#include + +#include "../../../src/cs-core/Settings.hpp" +#include "../../../src/cs-core/SolarSystem.hpp" +#include "../../../src/cs-graphics/GltfLoader.hpp" +//#include "../../../src/cs-graphics/internal/gltfmodel.hpp" +#include "../../../src/cs-utils/convert.hpp" +#include "../../../src/cs-utils/utils.hpp" + +#include +#include +#include + +#include "logger.hpp" +#include "utils.hpp" + +namespace csp::simpleobjects { + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SimpleObject::SimpleObject(std::string const& name, Plugin::Settings::SimpleObject const& config, + VistaSceneGraph* sceneGraph, std::shared_ptr settings, + std::shared_ptr solarSystem) + : mConfig(std::make_shared(config)) + , mSceneGraph(sceneGraph) + , mSettings(std::move(settings)) + , mSolarSystem(std::move(solarSystem)) + , mModel(std::make_shared(mConfig->mModelFile, mConfig->mEnvironmentMap /*, true*/)) { + + + mAnchorObject = mSolarSystem->getBody(mConfig->mAnchorName); + + mAnchor = std::unique_ptr(new cs::scene::CelestialAnchorNode(mSceneGraph->GetRoot(), mSceneGraph->GetNodeBridge(), name, + mSettings->getAnchorCenter(mConfig->mAnchorName), mSettings->getAnchorFrame(mConfig->mAnchorName))); + mSettings->initAnchor(*mAnchor, mConfig->mAnchorName); + mAnchor->setAnchorScale(mConfig->mScale.get()); + +// set initial position + auto lngLat = cs::utils::convert::toRadians(mConfig->mLngLat); + double height = (mAnchorObject->getHeight(lngLat) + mConfig->mElevation.get()) * mSettings->mGraphics.pHeightScale.get(); + mAnchor->setAnchorPosition(cs::utils::convert::toCartesian(lngLat, mAnchorObject->getRadii(), height)); + + // ground fixed rotation: + lastSurfaceNormal = cs::utils::convert::lngLatToNormal(cs::utils::convert::toRadians(mConfig->mLngLat)); + qRot = utils::normalToRotation(lastSurfaceNormal); + mAnchor->setAnchorRotation(qRot); + + mSolarSystem->registerAnchor(mAnchor); + mSceneGraph->GetRoot()->AddChild(mAnchor.get()); + + //mModel->setEnableHDR(true); + //mModel->setRotation(quat); + + mModel->setLightColor(1.0, 1.0, 1.0); + mModel->attachTo(mSceneGraph, mAnchor.get()); + + VistaOpenSGMaterialTools::SetSortKeyOnSubtree( + mAnchor.get(), static_cast(cs::utils::DrawOrder::eOpaqueItems)); + + + //auto modelSize = mModel->getBoundingBox()->GetDiagonalLength(); + //logger().info("Model size: {}", modelSize); + + editEnabled = false; + +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +SimpleObject::~SimpleObject() { + mSolarSystem->unregisterAnchor(mAnchor); + mSceneGraph->GetRoot()->DisconnectChild(mAnchor.get()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void SimpleObject::setSun(std::shared_ptr const& sun) { + mSun = sun; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::string SimpleObject::getName() const { + return mAnchor->GetName(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +bool SimpleObject::getIntersection( + glm::dvec3 const& /*rayPos*/, glm::dvec3 const& /*rayDir*/, glm::dvec3& /*pos*/) const { + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +double SimpleObject::getHeight(glm::dvec2 /*lngLat*/) const { + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + + +void SimpleObject::update(/*double tTime, cs::scene::CelestialObserver const& oObs*/) { + if(editEnabled) { return; } + + //mAnchor->SetIsEnabled(mAnchorObject->getIsInExistence() && mAnchorObject->pVisible.get()); + + if( !(mAnchor->getCenterName() == mSolarSystem->getObserver().getCenterName() && mAnchorObject->getIsInExistence()) ) { + mAnchor->SetIsEnabled(false); + return; + } + + /*VistaBoundingBox bb; + mModel->getShared()->GetBoundingBox(bb); + logger().info("model size: {}", bb.GetDiagonalLength());*/ + + + auto alpha_obj = atan(mConfig->mDiagonalLength.get() / glm::length(mSolarSystem->getObserver().getAnchorPosition() - mAnchor->getAnchorPosition())); + //auto pixels_diagonal = sqrt(mSettings->mGuiPosition->mWidthPixel * mSettings->mGuiPosition->mWidthPixel + mSettings->mGuiPosition->mHeightPixel * mSettings->mGuiPosition->mHeightPixel ); + + // may need to be adjusted for VR capability + auto focalLength = mSettings->mGraphics.pFocalLength.get(); + auto diagonal = mSettings->mGraphics.pSensorDiagonal.get(); + auto fovAngle = 2 * atan( diagonal / ( 2 * focalLength) ) * .0005; + + //logger().info("{}: a_img: {}, a_obj: {}, min: {}, enabled: {}", mAnchor->GetName(), alpha_img, alpha_obj, (alpha_img * .0005), (bool)(alpha_obj > alpha_img * .0005)); + + // TODO: enable if object would be visible -> better calculation needed + // Update position and rotation only if distance between observer and object is 100km or less + if( alpha_obj > fovAngle * .0005 ) { + + mAnchor->SetIsEnabled(true); + + // lngLat converted to radians + auto lngLat = cs::utils::convert::toRadians(mConfig->mLngLat); + + double height = (mAnchorObject->getHeight(lngLat) + mConfig->mElevation.get()) * mSettings->mGraphics.pHeightScale.get(); + mAnchor->setAnchorPosition(cs::utils::convert::toCartesian(lngLat, mAnchorObject->getRadii(), height)); + + + glm::dvec3 normal; + + if(mConfig->mAlignToSurface.get()) { + normal = utils::getSurfaceNormal(lngLat, mAnchorObject); + } else { + normal = cs::utils::convert::lngLatToNormal(lngLat); + } + + auto newRot = utils::normalToRotation(normal) * mConfig->mRotation.get(); + + if(newRot != qRot) { + qRot = newRot; + mAnchor->setAnchorRotation(qRot); + + logger().debug("Rotation of \"{}\" has been updated:", mAnchor->GetName()); + //auto normal = cs::utils::convert::lngLatToNormal(lngLat); + //logger().info(" normal x:{}, y:{}, z:{}", normal.x, normal.y, normal.z); + logger().debug(" Normal x:{}, y:{}, z:{}", normal.x, normal.y, normal.z); + logger().debug(" Rotation w:{}, x:{}, y:{}, z:{}", qRot.w, qRot.x, qRot.y, qRot.z); + + if(std::acos(glm::dot(lastSurfaceNormal, normal)) > 1.13 /* 65° in radians */ ) { + logger().warn("The rotation of \"{}\" has changed significantly. Maybe this should be verified.", mAnchor->GetName()); + } + + lastSurfaceNormal = normal; + } + } else { + mAnchor->SetIsEnabled(false); + //logger().info("{}: disabled", mAnchor->GetName()); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + + +void SimpleObject::setEditEnabled(bool enabled) { + + if(enabled) editEnabled = true; + else editEnabled = false; + + if(mAnchor != nullptr) { + mAnchor->SetIsEnabled(!enabled); + } +} + +bool SimpleObject::isEditedEnabled() const { + return editEnabled; +} + + +void SimpleObject::updateConfig(std::string const& name, Plugin::Settings::SimpleObject const& config) { + + // update config and reinitialize model + + std::shared_ptr newConfig = std::make_shared(config); + + if(name != getName()) { + mAnchor->SetName(name); + } + + if (newConfig->mModelFile != mConfig->mModelFile || + newConfig->mEnvironmentMap != mConfig->mEnvironmentMap || + newConfig->mAnchorName != mConfig->mAnchorName) { + + mSolarSystem->unregisterAnchor(mAnchor); + mSceneGraph->GetRoot()->DisconnectChild(mAnchor.get()); + + //mAnchor.reset(); + //mModel.reset(); + + + mAnchorObject = mSolarSystem->getBody(newConfig->mAnchorName); + mModel = std::make_shared(newConfig->mModelFile, newConfig->mEnvironmentMap /*, true*/); + + mAnchor = std::unique_ptr(new cs::scene::CelestialAnchorNode(mSceneGraph->GetRoot(), mSceneGraph->GetNodeBridge(), getName(), + mSettings->getAnchorCenter(newConfig->mAnchorName), mSettings->getAnchorFrame(newConfig->mAnchorName))); + + mSettings->initAnchor(*mAnchor, newConfig->mAnchorName); + mAnchor->setAnchorScale(newConfig->mScale.get()); + + + // set initial position + auto lngLat = cs::utils::convert::toRadians(newConfig->mLngLat); + double height = (mAnchorObject->getHeight(lngLat) + newConfig->mElevation.get()) * mSettings->mGraphics.pHeightScale.get(); + mAnchor->setAnchorPosition(cs::utils::convert::toCartesian(lngLat, mAnchorObject->getRadii(), height)); + + // ground fixed rotation: + lastSurfaceNormal = cs::utils::convert::lngLatToNormal(cs::utils::convert::toRadians(newConfig->mLngLat)); + qRot = utils::normalToRotation(lastSurfaceNormal); + mAnchor->setAnchorRotation(qRot); + + + mSolarSystem->registerAnchor(mAnchor); + mSceneGraph->GetRoot()->AddChild(mAnchor.get()); + + mModel->setLightColor(1.0, 1.0, 1.0); + mModel->attachTo(mSceneGraph, mAnchor.get()); + + } + + if(newConfig->mScale != mConfig->mScale) { + mAnchor->setAnchorScale(newConfig->mScale.get()); + } + + /*if(name != getName()) { + + mSolarSystem->unregisterAnchor(mAnchor); + mSceneGraph->GetRoot()->DisconnectChild(mAnchor.get());*/ + + // mModel = std::make_shared(mConfig->mModelFile, mConfig->mEnvironmentMap /*, true*/); + + // mAnchor = std::unique_ptr(new cs::scene::CelestialAnchorNode(mSceneGraph->GetRoot(), mSceneGraph->GetNodeBridge(), name, + // mSettings->getAnchorCenter(mConfig->mAnchorName), mSettings->getAnchorFrame(mConfig->mAnchorName))); + + // mSettings->initAnchor(*mAnchor, mConfig->mAnchorName); + + + //} + + +// auto lngLat = cs::utils::convert::toRadians(mConfig->mLngLat); + +// if(newConfig->mLngLat != mConfig->mLngLat) { +// double height = (mAnchorObject->getHeight(lngLat) + mConfig->mElevation.get()) * mSettings->mGraphics.pHeightScale.get(); +// mAnchor->setAnchorPosition(cs::utils::convert::toCartesian(lngLat, mAnchorObject->getRadii(), height)); +// } + +// // set initial position + + +// // ground fixed rotation: +// if(newConfig->mRotation != mConfig->mRotation) { +// lastSurfaceNormal = cs::utils::convert::lngLatToNormal(cs::utils::convert::toRadians(mConfig->mLngLat)); +// qRot = utils::normalToRotation(lastSurfaceNormal); +// mAnchor->setAnchorRotation(qRot); + +// } + + mConfig = std::move(newConfig); + +} + + + +} // namespace csp::simpleobjects diff --git a/plugins/csp-simple-objects/src/SimpleObject.hpp b/plugins/csp-simple-objects/src/SimpleObject.hpp new file mode 100644 index 000000000..f2a7d6125 --- /dev/null +++ b/plugins/csp-simple-objects/src/SimpleObject.hpp @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CSP_SIMPLE_OBJECTS_SIMPLEOBJECT_HPP +#define CSP_SIMPLE_OBJECTS_SIMPLEOBJECT_HPP + +#include "Plugin.hpp" + +#include "../../../src/cs-core/Settings.hpp" +#include "../../../src/cs-scene/CelestialBody.hpp" + +#include "../../../src/cs-scene/CelestialAnchorNode.hpp" +#include + +namespace cs::graphics { +class GltfLoader; +} // namespace cs::graphics + +namespace cs::core { +class Settings; +class SolarSystem; +} // namespace cs::core + +class VistaTransformNode; + +namespace csp::simpleobjects { + +/// A simple object on another celestial body. +class SimpleObject { + public: + + /// name: Displayed name of the object + /// config: Plugin::Settings::SimpleObject settings defined in .json + SimpleObject(std::string const& name, Plugin::Settings::SimpleObject const& config, + VistaSceneGraph* sceneGraph, std::shared_ptr settings, + std::shared_ptr solarSystem); + + SimpleObject(SimpleObject const& other) = delete; + SimpleObject(SimpleObject&& other) = default; + + SimpleObject& operator=(SimpleObject const& other) = delete; + SimpleObject& operator=(SimpleObject&& other) = delete; + + ~SimpleObject(); + + void update(/*double tTime, cs::scene::CelestialObserver const& oObs*/); + + void setSun(std::shared_ptr const& sun); + + void updateConfig(std::string const& name, Plugin::Settings::SimpleObject const& config); + + std::string getName() const; + + // interface of scene::CelestialBody --------------------------------------- + + bool getIntersection( + glm::dvec3 const& rayPos, glm::dvec3 const& rayDir, glm::dvec3& pos) const; + double getHeight(glm::dvec2 lngLat) const; + + void setEditEnabled(bool enabled); + bool isEditedEnabled() const; + + private: + std::shared_ptr mConfig; + VistaSceneGraph* mSceneGraph; + std::shared_ptr mSettings; + std::shared_ptr mSolarSystem; + std::shared_ptr mAnchor; + std::shared_ptr mAnchorObject; + std::shared_ptr mModel; + std::shared_ptr mSun; + + glm::dquat qRot; + glm::dvec3 lastSurfaceNormal; + + bool editEnabled = false; +}; + + +/* +/// A single satellite within the Solar System. +class Satellite : public cs::scene::CelestialBody { + public: + Satellite(Plugin::Settings::SimpleObject const& config, std::string const& anchorName, + VistaSceneGraph* sceneGraph, std::shared_ptr settings, + std::shared_ptr solarSystem); + + Satellite(Satellite const& other) = delete; + Satellite(Satellite&& other) = default; + + Satellite& operator=(Satellite const& other) = delete; + Satellite& operator=(Satellite&& other) = delete; + + ~Satellite() override; + + void update(double tTime, cs::scene::CelestialObserver const& oObs) override; + + void setSun(std::shared_ptr const& sun); + + // interface of scene::CelestialBody --------------------------------------- + + bool getIntersection( + glm::dvec3 const& rayPos, glm::dvec3 const& rayDir, glm::dvec3& pos) const override; + double getHeight(glm::dvec2 lngLat) const override; + + private: + VistaSceneGraph* mSceneGraph; + std::shared_ptr mSettings; + std::shared_ptr mSolarSystem; + std::unique_ptr mAnchor; + std::unique_ptr mModel; + std::shared_ptr mSun; +}; */ + + +} // namespace csp::simpleobjects + +#endif // CSP_SIMPLE_OBJECTS_SIMPLEOBJECT_HPP diff --git a/plugins/csp-simple-objects/src/logger.cpp b/plugins/csp-simple-objects/src/logger.cpp new file mode 100644 index 000000000..f68011f31 --- /dev/null +++ b/plugins/csp-simple-objects/src/logger.cpp @@ -0,0 +1,22 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "logger.hpp" + +#include "../../../src/cs-utils/logger.hpp" + +namespace csp::simpleobjects { + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +spdlog::logger& logger() { + static auto logger = cs::utils::createLogger("csp-simple-objects"); + return *logger; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +} // namespace csp::simpleobjects diff --git a/plugins/csp-simple-objects/src/logger.hpp b/plugins/csp-simple-objects/src/logger.hpp new file mode 100644 index 000000000..1c2db969d --- /dev/null +++ b/plugins/csp-simple-objects/src/logger.hpp @@ -0,0 +1,20 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CSP_SIMPLE_OBJECTS_LOGGER_HPP +#define CSP_SIMPLE_OBJECTS_LOGGER_HPP + +#include + +namespace csp::simpleobjects { + +/// This creates the default singleton logger for "csp-simple-objects" when called for the first time +/// and returns it. See cs-utils/logger.hpp for more logging details. +spdlog::logger& logger(); + +} // namespace csp::simpleobjects + +#endif // CSP_SIMPLE_OBJECTS_LOGGER_HPP diff --git a/plugins/csp-simple-objects/src/utils.cpp b/plugins/csp-simple-objects/src/utils.cpp new file mode 100644 index 000000000..49ac6e19d --- /dev/null +++ b/plugins/csp-simple-objects/src/utils.cpp @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "utils.hpp" +#include "logger.hpp" + +#include "../../../src/cs-scene/CelestialBody.hpp" +#include "../../../src/cs-utils/convert.hpp" + +#include +#include + +namespace csp::simpleobjects::utils { + + using glm::dvec3, glm::dvec2; + + + dvec3 getSurfaceNormal(const dvec2 &lngLat, std::shared_ptr &body, const double offset) { + + dvec3 radii = body->getRadii(); + double offsetAngle; + + if( radii[0] == radii[1] && radii[0] == radii[2] ) { + offsetAngle = offset / radii[0]; + } else { + offsetAngle = offset / sqrt( (radii[0] * radii[0] + radii[1] * radii[1] + radii[2] * radii[2]) / 3.0F ); + } + + dvec2 offLL1(lngLat.x - offsetAngle, lngLat.y); + dvec2 offLL2(lngLat.x + offsetAngle, lngLat.y); + dvec2 offLL3(lngLat.x, lngLat.y - offsetAngle); + dvec2 offLL4(lngLat.x, lngLat.y + offsetAngle); + + dvec3 surfaceVec1 = getLngLatPositionOnBody(offLL2, body) - getLngLatPositionOnBody(offLL1, body); + dvec3 surfaceVec2 = getLngLatPositionOnBody(offLL4, body) - getLngLatPositionOnBody(offLL3, body); + + return glm::normalize(glm::cross(surfaceVec1, surfaceVec2)); + } + + + dvec3 getLngLatPositionOnBody(const dvec2 &lngLat, std::shared_ptr &body) { + return cs::utils::convert::toCartesian(lngLat, body->getRadii(), body->getHeight(lngLat)); + } + + + glm::dquat normalToRotation(const dvec3 &normal) { + + dvec3 north = dvec3(0,1,0); + dvec3 z = glm::normalize(glm::cross(north, normal)); + north = glm::normalize(glm::cross(normal, z)); + + return glm::toQuat(glm::dmat3(north, normal, z)); + } + + +} // namespace cs::simpleobjects::utils \ No newline at end of file diff --git a/plugins/csp-simple-objects/src/utils.hpp b/plugins/csp-simple-objects/src/utils.hpp new file mode 100644 index 000000000..57339582d --- /dev/null +++ b/plugins/csp-simple-objects/src/utils.hpp @@ -0,0 +1,32 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////// +// This file is part of CosmoScout VR // +// and may be used under the terms of the MIT license. See the LICENSE file for details. // +// Copyright: (c) 2022 German Aerospace Center (DLR) // +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifndef CSP_SIMPLE_OBJECTS_UTILS_HPP +#define CSP_SIMPLE_OBJECTS_UTILS_HPP + +#include +#include +#include "../../../src/cs-scene/CelestialBody.hpp" + +namespace csp::simpleobjects { + +namespace utils { + + // Returns the normalized normal of a sloped surface. Is usually used with lod bodies. + // Offset in meters + glm::dvec3 getSurfaceNormal(const glm::dvec2 &lngLat, std::shared_ptr &body, const double offset = 0.9F); + + // wrapper around cs::utils::convert::toCartesian for easier + // access to position from getSurfaceNormal() + glm::dvec3 getLngLatPositionOnBody(const glm::dvec2 &lngLat, std::shared_ptr &body); + + glm::dquat normalToRotation(const glm::dvec3 &normal); + +} //namespace utils + +} // namespace csp::simplobjects + +#endif // CSP_SIMPLE_OBJECTS_UTILS_HPP \ No newline at end of file