// // Application_Entities.cpp // interface/src // // Split from Application.cpp by HifiExperiments on 3/30/24 // Created by Andrzej Kapolka on 5/10/13. // Copyright 2013 High Fidelity, Inc. // Copyright 2020 Vircadia contributors. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #include "Application.h" #include #include #include #include #include #include #include #include "InterfaceLogging.h" #include "LODManager.h" static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { if (maxOctreePPS != _maxOctreePPS) { _maxOctreePPS = maxOctreePPS; _maxOctreePacketsPerSecond.set(_maxOctreePPS); } } QVector Application::pasteEntities(const QString& entityHostType, float x, float y, float z) { return _entityClipboard->sendEntities(_entityEditSender.get(), getEntities()->getTree(), entityHostType, x, y, z); } bool Application::exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset, const QVariantMap& options) { QVector propertiesToPrune = { "queryAACube", "age", "created", "lastEdited", "lastEditedBy", "faceCamera", "isFacingAvatar", "clientOnly", "avatarEntity", "localEntity", }; bool globalPositions = options.value("globalPositions", false).toBool(); auto paths = options.value("paths", QVariantMap()).toMap(); QHash entities; auto nodeList = DependencyManager::get(); const QUuid myAvatarID = nodeList->getSessionUUID(); auto entityTree = getEntities()->getTree(); auto exportTree = std::make_shared(); exportTree->setMyAvatar(getMyAvatar()); exportTree->createRootElement(); glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); bool success = true; entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree, globalPositions] { for (auto entityID : entityIDs) { // Gather entities and properties. auto entityItem = entityTree->findEntityByEntityItemID(entityID); if (!entityItem) { qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; continue; } if (!givenOffset && !globalPositions) { EntityItemID parentID = entityItem->getParentID(); bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); if (!parentIsAvatar && (parentID.isInvalidID() || !entityIDs.contains(parentID) || !entityTree->findEntityByEntityItemID(parentID))) { // If parent wasn't selected, we want absolute position, which isn't in properties. auto position = entityItem->getWorldPosition(); root.x = glm::min(root.x, position.x); root.y = glm::min(root.y, position.y); root.z = glm::min(root.z, position.z); } } entities[entityID] = entityItem; } if (entities.size() == 0) { success = false; return; } if (globalPositions) { root = {0.0f, 0.0f, 0.0f}; } else if (givenOffset) { root = *givenOffset; } for (EntityItemPointer& entityDatum : entities) { auto properties = entityDatum->getProperties(); EntityItemID parentID = properties.getParentID(); bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); if (parentIsAvatar) { properties.setParentID(AVATAR_SELF_ID); } else { if (parentID.isInvalidID()) { properties.setPosition(properties.getPosition() - root); } else if (!entities.contains(parentID)) { entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); } // else valid parent -- don't offset } exportTree->addEntity(entityDatum->getEntityItemID(), properties); } }); if (success) { QJsonDocument jsonDocument; bool success = exportTree->toJSONDocument(&jsonDocument); if (!success) { return false; } auto object = jsonDocument.object(); if (propertiesToPrune.size() != 0) { auto entityList = object["Entities"].toArray(); for (int i = 0; i < entityList.size(); i++) { auto entity = entityList[i].toObject(); for (auto property : propertiesToPrune) { entity.remove(property); } entityList[i] = entity; } // these have to be re-assigned rather than modified by reference // because it looks like Qt only gives us copies object["Entities"] = entityList; } if (!paths.isEmpty()) { QJsonObject pathsObject; foreach (const QString& key, paths.keys()) { auto object = paths.value(key).toMap(); auto position = object.value("position").toMap(); auto orientation = object.value("orientation").toMap(); pathsObject[key] = QString("/%1,%2,%3/%4,%5,%6,%7") .arg(position["x"].toDouble()) .arg(position["y"].toDouble()) .arg(position["z"].toDouble()) .arg(orientation["x"].toDouble()) .arg(orientation["y"].toDouble()) .arg(orientation["z"].toDouble()) .arg(orientation["w"].toDouble()); } object["Paths"] = pathsObject; } // weirdly QJsonDocument::object returns a copy and not a reference jsonDocument.setObject(object); // Octree::writeToJSONFile QSaveFile persistFile(filename); if (persistFile.open(QIODevice::WriteOnly)) { if (persistFile.write(jsonDocument.toJson()) != -1) { success = persistFile.commit(); if (!success) { qCritical() << "Failed to commit to JSON save file:" << persistFile.errorString(); } } else { qCritical("Failed to write to JSON file."); } } else { qCritical("Failed to open JSON file for writing."); } // restore the main window's active state #ifdef USE_GL _window->activateWindow(); //VKTODO #endif } return success; } bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale, const QVariantMap& options) { glm::vec3 center(x, y, z); glm::vec3 minCorner = center - vec3(scale); float cubeSize = scale * 2; AACube boundingCube(minCorner, cubeSize); QVector entities; auto entityTree = getEntities()->getTree(); entityTree->withReadLock([&] { entityTree->evalEntitiesInCube( boundingCube, PickFilter(PickFilter::getBitMask(PickFilter::DOMAIN_ENTITIES)), entities ); }); return exportEntities(filename, entities, ¢er, options); } bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { bool success = false; _entityClipboard->withWriteLock([&] { _entityClipboard->eraseAllOctreeElements(); // FIXME: readFromURL() can take over the main event loop which may cause problems, especially if downloading the JSON // from the Web. success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId, true); if (success) { _entityClipboard->reaverageOctreeElements(); } }); return success; } void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { if (qApp->getLoginDialogPoppedUp()) { return; } auto entityScriptingInterface = DependencyManager::get(); if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { EntityItemProperties properties; properties.setType(EntityTypes::Box); properties.setAlpha(1.0f); properties.setColor({ 0xFF, 0xEF, 0x00 }); properties.setPrimitiveMode(PrimitiveMode::LINES); properties.getPulse().setMin(0.5); properties.getPulse().setMax(1.0f); properties.getPulse().setColorMode(PulseMode::IN_PHASE); properties.setIgnorePickIntersection(true); _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); } // Position focus EntityItemProperties properties; properties.setPosition(position); properties.setRotation(rotation); properties.setDimensions(dimensions); properties.setVisible(true); entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); } void Application::setKeyboardFocusEntity(const QUuid& id) { if (_keyboardFocusedEntity.get() != id) { if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { if (id == _loginDialogID) { emit loginDialogFocusEnabled(); } else if (!_keyboardFocusWaitingOnRenderable) { // that's the only entity we want in focus; return; } } _keyboardFocusedEntity.set(id); auto entityScriptingInterface = DependencyManager::get(); if (id != UNKNOWN_ENTITY_ID) { EntityPropertyFlags desiredProperties; desiredProperties += PROP_VISIBLE; desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; auto properties = entityScriptingInterface->getEntityProperties(id); if (properties.getVisible()) { auto entities = getEntities(); auto entityId = _keyboardFocusedEntity.get(); auto entityItemRenderable = entities->renderableForEntityId(entityId); if (!entityItemRenderable) { _keyboardFocusWaitingOnRenderable = true; } else if (entityItemRenderable->wantsKeyboardFocus()) { entities->setProxyWindow(entityId, _window->windowHandle()); if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->pluginFocusOutEvent(); } _lastAcceptedKeyPress = usecTimestampNow(); if (properties.getShowKeyboardFocusHighlight()) { if (auto entity = entities->getEntity(entityId)) { setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); return; } } } } } EntityItemProperties properties; properties.setVisible(false); entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); } } void Application::clearDomainOctreeDetails(bool clearAll) { // if we're about to quit, we really don't need to do the rest of these things... if (_aboutToQuit) { return; } qCDebug(interfaceapp) << "Clearing domain octree details..."; _waitForServerlessToBeSet = true; resetPhysicsReadyInformation(); setIsInterstitialMode(true); auto octreeServerSceneStats = getOcteeSceneStats(); octreeServerSceneStats->withWriteLock([&] { octreeServerSceneStats->clear(); }); // reset the model renderer clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); ShaderCache::instance().clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); } void Application::resettingDomain() { _notifiedPacketVersionMismatchThisDomain = false; clearDomainOctreeDetails(false); } void Application::queryOctree(NodeType_t serverType, PacketType packetType) { if (!_settingsLoaded) { return; // bail early if settings are not loaded } const bool isModifiedQuery = !_physicsEnabled; if (isModifiedQuery) { if (!_octreeProcessor->safeLandingIsActive()) { // don't send the octreeQuery until SafeLanding knows it has started return; } // Create modified view that is a simple sphere. bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); ConicalViewFrustum sphericalView; AABox box = getMyAvatar()->getGlobalBoundingBox(); float radius = glm::max(INITIAL_QUERY_RADIUS, 0.5f * glm::length(box.getDimensions())); sphericalView.setPositionAndSimpleRadius(box.calcCenter(), radius); if (interstitialModeEnabled) { ConicalViewFrustum farView; farView.set(_viewFrustum); _octreeQuery.setConicalViews({ sphericalView, farView }); } else { _octreeQuery.setConicalViews({ sphericalView }); } _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); static constexpr float MIN_LOD_ADJUST = -20.0f; _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); } else { _octreeQuery.setConicalViews(_conicalViews); auto lodManager = DependencyManager::get(); _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); } _octreeQuery.setReportInitialCompletion(isModifiedQuery); auto nodeList = DependencyManager::get(); auto node = nodeList->soloNodeOfType(serverType); if (node && node->getActiveSocket()) { _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); auto queryPacket = NLPacket::create(packetType); // encode the query data auto packetData = reinterpret_cast(queryPacket->getPayload()); int packetSize = _octreeQuery.getBroadcastData(packetData); queryPacket->setPayloadSize(packetSize); // make sure we still have an active socket nodeList->sendUnreliablePacket(*queryPacket, *node); } } int Application::sendNackPackets() { // iterates through all nodes in NodeList auto nodeList = DependencyManager::get(); int packetsSent = 0; nodeList->eachNode([&](const SharedNodePointer& node){ if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); QUuid nodeUUID = node->getUUID(); // if there are octree packets from this node that are waiting to be processed, // don't send a NACK since the missing packets may be among those waiting packets. if (_octreeProcessor->hasPacketsToProcessFrom(nodeUUID)) { return; } QSet missingSequenceNumbers; auto octreeServerSceneStats = getOcteeSceneStats(); octreeServerSceneStats->withReadLock([&] { // retrieve octree scene stats of this node if (octreeServerSceneStats->find(nodeUUID) == octreeServerSceneStats->end()) { return; } // get sequence number stats of node, prune its missing set, and make a copy of the missing set SequenceNumberStats& sequenceNumberStats = (*octreeServerSceneStats)[nodeUUID].getIncomingOctreeSequenceNumberStats(); sequenceNumberStats.pruneMissingSet(); missingSequenceNumbers = sequenceNumberStats.getMissingSet(); }); _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); // construct nack packet(s) for this node foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { nackPacketList->writePrimitive(missingNumber); } if (nackPacketList->getNumPackets()) { packetsSent += (int)nackPacketList->getNumPackets(); // send the packet list nodeList->sendPacketList(std::move(nackPacketList), *node); } } }); return packetsSent; }