diff --git a/CMakeLists.txt b/CMakeLists.txt index bc67d622de..5deecb4be5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,8 +88,17 @@ endif () set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${QT_CMAKE_PREFIX_PATH}) -# set our OS X deployment target to -set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) +if (APPLE) + # set our OS X deployment target + set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) + + # find the 10.9 SDK path + execute_process(COMMAND xcodebuild -sdk -version OUTPUT_VARIABLE XCODE_SDK_VERSIONS) + string(REGEX MATCH \\/.+MacOSX10.9.sdk OSX_SDK_PATH ${XCODE_SDK_VERSIONS}) + + # set that as the SDK to use + set(CMAKE_OSX_SYSROOT ${OSX_SDK_PATH}) +endif () # Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) diff --git a/examples/editEntities.js b/examples/editEntities.js index 61100b4556..9b13b167e7 100644 --- a/examples/editEntities.js +++ b/examples/editEntities.js @@ -396,6 +396,10 @@ var toolBar = (function () { return handled; } + Window.domainChanged.connect(function() { + that.setActive(false); + }); + that.cleanup = function () { toolBar.cleanup(); }; diff --git a/examples/notifications.js b/examples/notifications.js index f9d2ba83ed..27192ed789 100644 --- a/examples/notifications.js +++ b/examples/notifications.js @@ -38,11 +38,13 @@ // function onIncomingMessage(user, message) { // //do stuff here; // var text = "This is a notification"; -// wordWrap(text); +// var wrappedText = wordWrap(text); +// createNotification(wrappedText, NotificationType.SNAPSHOT); // } // // This new function must call wordWrap(text) if the length of message is longer than 42 chars or unknown. -// wordWrap() will format the text to fit the notifications overlay and send it to createNotification(text). +// wordWrap() will format the text to fit the notifications overlay and return it +// after that we will send it to createNotification(text). // If the message is 42 chars or less you should bypass wordWrap() and call createNotification() directly. @@ -50,12 +52,12 @@ // // 1. Add a key to the keyPressEvent(key). // 2. Declare a text string. -// 3. Call createNotifications(text) parsing the text. +// 3. Call createNotifications(text, NotificationType) parsing the text. // example: // var welcome; // if (key.text == "q") { //queries number of users online // var welcome = "There are " + GlobalServices.onlineUsers.length + " users online now."; -// createNotification(welcome); +// createNotification(welcome, NotificationType.USERS_ONLINE); // } Script.include("./libraries/globals.js"); Script.include("./libraries/soundArray.js"); @@ -83,6 +85,46 @@ var last_users = GlobalServices.onlineUsers; var users = []; var ctrlIsPressed = false; var ready = true; +var MENU_NAME = 'Tools > Notifications'; +var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; +var NOTIFICATION_MENU_ITEM_POST = " Notifications"; +var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; +var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; + +var NotificationType = { + UNKNOWN: 0, + USER_JOINS: 1, + USER_LEAVES: 2, + MUTE_TOGGLE: 3, + CHAT_MENTION: 4, + USERS_ONLINE: 5, + SNAPSHOT: 6, + WINDOW_RESIZE: 7, + properties: [ + { text: "User Join" }, + { text: "User Leave" }, + { text: "Mute Toggle" }, + { text: "Chat Mention" }, + { text: "Users Online" }, + { text: "Snapshot" }, + { text: "Window Resize" } + ], + getTypeFromMenuItem: function(menuItemName) { + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { + return NotificationType.UNKNOWN; + } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function(type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; + } +}; var randomSounds = new SoundArray({ localOnly: true }, true); var numberOfSounds = 2; @@ -90,15 +132,6 @@ for (var i = 1; i <= numberOfSounds; i++) { randomSounds.addSound(HIFI_PUBLIC_BUCKET + "sounds/UI/notification-general" + i + ".raw"); } -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); - } -} -Script.scriptEnding.connect(scriptEnding); - var notifications = []; var buttons = []; var times = []; @@ -211,8 +244,6 @@ function notify(notice, button, height) { positions, last; - randomSounds.playRandom(); - if (isOnHMD) { // Calculate 3D values from 2D overlay properties. @@ -257,7 +288,7 @@ function notify(notice, button, height) { } // This function creates and sizes the overlays -function createNotification(text) { +function createNotification(text, notificationType) { var count = (text.match(/\n/g) || []).length, breakPoint = 43.0, // length when new line is added extraLine = 0, @@ -307,6 +338,12 @@ function createNotification(text) { alpha: backgroundAlpha }; + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) + { + randomSounds.playRandom(); + } + notify(noticeProperties, buttonProperties, height); } @@ -345,8 +382,7 @@ function stringDivider(str, slotWidth, spaceReplacer) { // formats string to add newline every 43 chars function wordWrap(str) { - var result = stringDivider(str, 43.0, "\n"); - createNotification(result); + return stringDivider(str, 43.0, "\n"); } // This fires a notification on window resize @@ -358,7 +394,7 @@ function checkSize() { windowDimensions = Controller.getViewportDimensions(); overlayLocationX = (windowDimensions.x - (width + 60.0)); buttonLocationX = overlayLocationX + (width - 35.0); - createNotification(windowResize); + createNotification(windowResize, NotificationType.WINDOW_RESIZE); } } @@ -442,10 +478,7 @@ var STARTUP_TIMEOUT = 500, // ms // This reports the number of users online at startup function reportUsers() { - var welcome; - - welcome = "Welcome! There are " + GlobalServices.onlineUsers.length + " users online now."; - createNotification(welcome); + createNotification("Welcome! There are " + GlobalServices.onlineUsers.length + " users online now.", NotificationType.USERS_ONLINE); } function finishStartup() { @@ -472,13 +505,13 @@ function onOnlineUsersChanged(users) { if (!isStartingUp()) { // Skip user notifications at startup. for (i = 0; i < users.length; i += 1) { if (last_users.indexOf(users[i]) === -1.0) { - createNotification(users[i] + " has joined"); + createNotification(users[i] + " has joined", NotificationType.USER_JOINS); } } for (i = 0; i < last_users.length; i += 1) { if (users.indexOf(last_users[i]) === -1.0) { - createNotification(last_users[i] + " has left"); + createNotification(last_users[i] + " has left", NotificationType.USER_LEAVES); } } } @@ -497,7 +530,7 @@ function onIncomingMessage(user, message) { thisAlert = user + ": " + myMessage; if (myMessage.indexOf(alertMe) > -1.0) { - wordWrap(thisAlert); + CreateNotification(wordWrap(thisAlert), NotificationType.CHAT_MENTION); } } @@ -506,9 +539,9 @@ function onMuteStateChanged() { var muteState, muteString; - muteState = AudioDevice.getMuted() ? "muted" : "unmuted"; + muteState = AudioDevice.getMuted() ? "muted" : "unmuted"; muteString = "Microphone is now " + muteState; - createNotification(muteString); + createNotification(muteString, NotificationType.MUTE_TOGGLE); } // handles mouse clicks on buttons @@ -551,25 +584,58 @@ function keyPressEvent(key) { if (key.text === "q") { //queries number of users online numUsers = GlobalServices.onlineUsers.length; welcome = "There are " + numUsers + " users online now."; - createNotification(welcome); + createNotification(welcome, NotificationType.USERS_ONLINE); } if (key.text === "s") { if (ctrlIsPressed === true) { noteString = "Snapshot taken."; - createNotification(noteString); + createNotification(noteString, NotificationType.SNAPSHOT); } } } +function setup() { + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, + isCheckable: true, + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) + }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + isCheckable: true, + isChecked: checked + }); + } +} + // When our script shuts down, we should clean up all of our overlays function scriptEnding() { - var i; - - for (i = 0; i < notifications.length; i += 1) { + for (var i = 0; i < notifications.length; i++) { Overlays.deleteOverlay(notifications[i]); Overlays.deleteOverlay(buttons[i]); } + Menu.removeMenu(MENU_NAME); +} + +function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; + } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } } AudioDevice.muteToggled.connect(onMuteStateChanged); @@ -580,3 +646,6 @@ GlobalServices.incomingMessage.connect(onIncomingMessage); Controller.keyReleaseEvent.connect(keyReleaseEvent); Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); +Menu.menuItemEvent.connect(menuItemEvent); + +setup(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a7e73da1bd..d3161ebf51 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -533,6 +533,10 @@ void Application::aboutToQuit() { } void Application::cleanupBeforeQuit() { + + _entities.shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + ScriptEngine::stopAllScripts(this); // stop all currently running global scripts + // first stop all timers directly or by invokeMethod // depending on what thread they run in locationUpdateTimer->stop(); @@ -597,13 +601,11 @@ Application::~Application() { DependencyManager::destroy(); - qDebug() << "start destroying ResourceCaches Application::~Application() line:" << __LINE__; DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - qDebug() << "done destroying ResourceCaches Application::~Application() line:" << __LINE__; } void Application::initializeGL() { @@ -1469,6 +1471,10 @@ void Application::checkFPS() { void Application::idle() { PerformanceTimer perfTimer("idle"); + + if (_aboutToQuit) { + return; // bail early, nothing to do here. + } // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing // details if we're in ExtraDebugging mode. However, the ::update() and it's subcomponents will show their timing @@ -3535,19 +3541,20 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("MIDI", &MIDIManager::getInstance()); #endif + // TODO: Consider moving some of this functionality into the ScriptEngine class instead. It seems wrong that this + // work is being done in the Application class when really these dependencies are more related to the ScriptEngine's + // implementation QThread* workerThread = new QThread(this); - workerThread->setObjectName("Script Engine Thread"); + QString scriptEngineName = QString("Script Thread:") + scriptEngine->getFilename(); + workerThread->setObjectName(scriptEngineName); // when the worker thread is started, call our engine's run.. connect(workerThread, &QThread::started, scriptEngine, &ScriptEngine::run); // when the thread is terminated, add both scriptEngine and thread to the deleteLater queue - connect(scriptEngine, SIGNAL(finished(const QString&)), scriptEngine, SLOT(deleteLater())); + connect(scriptEngine, SIGNAL(doneRunning()), scriptEngine, SLOT(deleteLater())); connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater())); - // when the application is about to quit, stop our script engine so it unwinds properly - connect(this, SIGNAL(aboutToQuit()), scriptEngine, SLOT(stop())); - auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::nodeKilled, scriptEngine, &ScriptEngine::nodeKilled); @@ -3558,7 +3565,12 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri } ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUserLoaded, - bool loadScriptFromEditor, bool activateMainWindow) { + bool loadScriptFromEditor, bool activateMainWindow) { + + if (isAboutToQuit()) { + return NULL; + } + QUrl scriptUrl(scriptFilename); const QString& scriptURLString = scriptUrl.toString(); if (_scriptEnginesHash.contains(scriptURLString) && loadScriptFromEditor @@ -3749,8 +3761,8 @@ void Application::domainSettingsReceived(const QJsonObject& domainSettingsObject voxelWalletUUID = QUuid(voxelObject[VOXEL_WALLET_UUID].toString()); } - qDebug() << "Voxel costs are" << satoshisPerVoxel << "per voxel and" << satoshisPerMeterCubed << "per meter cubed"; - qDebug() << "Destination wallet UUID for voxel payments is" << voxelWalletUUID; + qDebug() << "Octree edits costs are" << satoshisPerVoxel << "per octree cell and" << satoshisPerMeterCubed << "per meter cubed"; + qDebug() << "Destination wallet UUID for edit payments is" << voxelWalletUUID; } QString Application::getPreviousScriptLocation() { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index c4b288347d..fc3eefe260 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -18,6 +18,7 @@ #include #include "Application.h" +#include "DomainHandler.h" #include "MainWindow.h" #include "Menu.h" #include "ui/ModelsBrowser.h" @@ -34,6 +35,8 @@ WindowScriptingInterface::WindowScriptingInterface() : _nonBlockingFormActive(false), _formResult(QDialog::Rejected) { + const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); + connect(&domainHandler, &DomainHandler::hostnameChanged, this, &WindowScriptingInterface::domainChanged); } WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 66d0ab9306..5c0aa4f0a8 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -57,6 +57,7 @@ public slots: QScriptValue peekNonBlockingFormResult(QScriptValue array); signals: + void domainChanged(const QString& domainHostname); void inlineButtonClicked(const QString& name); void nonBlockingFormClosed(); diff --git a/interface/src/ui/overlays/BillboardOverlay.cpp b/interface/src/ui/overlays/BillboardOverlay.cpp index 27bd0c9102..a82da0ee5f 100644 --- a/interface/src/ui/overlays/BillboardOverlay.cpp +++ b/interface/src/ui/overlays/BillboardOverlay.cpp @@ -10,6 +10,8 @@ // #include "Application.h" +#include "GeometryUtil.h" +#include "PlaneShape.h" #include "BillboardOverlay.h" @@ -191,19 +193,25 @@ bool BillboardOverlay::findRayIntersection(const glm::vec3& origin, const glm::v float& distance, BoxFace& face) { if (_texture) { + glm::quat rotation; + if (_isFacingAvatar) { + // rotate about vertical to face the camera + rotation = Application::getInstance()->getCamera()->getRotation(); + rotation *= glm::angleAxis(glm::pi(), glm::vec3(0.0f, 1.0f, 0.0f)); + } else { + rotation = _rotation; + } + + // Produce the dimensions of the billboard based on the image's aspect ratio and the overlay's scale. bool isNull = _fromImage.isNull(); float width = isNull ? _texture->getWidth() : _fromImage.width(); float height = isNull ? _texture->getHeight() : _fromImage.height(); - float maxSize = glm::max(width, height); - float x = width / (2.0f * maxSize); - float y = -height / (2.0f * maxSize); - float maxDimension = glm::max(x,y); - float scaledDimension = maxDimension * _scale; - glm::vec3 corner = getCenter() - glm::vec3(scaledDimension, scaledDimension, scaledDimension) ; - AACube myCube(corner, scaledDimension * 2.0f); - return myCube.findRayIntersection(origin, direction, distance, face); + glm::vec2 dimensions = _scale * glm::vec2(width / maxSize, height / maxSize); + + return findRayRectangleIntersection(origin, direction, rotation, _position, dimensions); } + return false; } diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index 508b5c4227..925725d261 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -17,6 +17,8 @@ #include #include +#include "GeometryUtil.h" + #include "Planar3DOverlay.h" const float DEFAULT_SIZE = 1.0f; @@ -93,29 +95,5 @@ QScriptValue Planar3DOverlay::getProperty(const QString& property) { bool Planar3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face) { - RayIntersectionInfo rayInfo; - rayInfo._rayStart = origin; - rayInfo._rayDirection = direction; - rayInfo._rayLength = std::numeric_limits::max(); - - PlaneShape plane; - - const glm::vec3 UNROTATED_NORMAL(0.0f, 0.0f, -1.0f); - glm::vec3 normal = _rotation * UNROTATED_NORMAL; - plane.setNormal(normal); - plane.setPoint(_position); // the position is definitely a point on our plane - - bool intersects = plane.findRayIntersection(rayInfo); - - if (intersects) { - distance = rayInfo._hitDistance; - - glm::vec3 hitPosition = origin + (distance * direction); - glm::vec3 localHitPosition = glm::inverse(_rotation) * (hitPosition - _position); - glm::vec2 halfDimensions = _dimensions / 2.0f; - - intersects = -halfDimensions.x <= localHitPosition.x && localHitPosition.x <= halfDimensions.x - && -halfDimensions.y <= localHitPosition.y && localHitPosition.y <= halfDimensions.y; - } - return intersects; + return findRayRectangleIntersection(origin, direction, _rotation, _position, _dimensions); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 975d0f515d..46f9ff6f55 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -59,10 +59,17 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf } EntityTreeRenderer::~EntityTreeRenderer() { - // NOTE: we don't need to delete _entitiesScriptEngine because it's owned by the application and gets cleaned up - // automatically but we do need to delete our sandbox script engine. - delete _sandboxScriptEngine; - _sandboxScriptEngine = NULL; + // NOTE: we don't need to delete _entitiesScriptEngine because it is registered with the application and has a + // signal tied to call it's deleteLater on doneRunning + if (_sandboxScriptEngine) { + // TODO: consider reworking how _sandboxScriptEngine is managed. It's treated differently than _entitiesScriptEngine + // because we don't call registerScriptEngineWithApplicationServices() for it. This implementation is confusing and + // potentially error prone because it's not a full fledged ScriptEngine that has been fully connected to the + // application. We did this so that scripts that were ill-formed could be evaluated but not execute against the + // application services. But this means it's shutdown behavior is different from other ScriptEngines + delete _sandboxScriptEngine; + _sandboxScriptEngine = NULL; + } } void EntityTreeRenderer::clear() { @@ -97,6 +104,11 @@ void EntityTreeRenderer::init() { connect(entityTree, &EntityTree::changingEntityID, this, &EntityTreeRenderer::changingEntityID); } +void EntityTreeRenderer::shutdown() { + _shuttingDown = true; +} + + QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID) { EntityItem* entity = static_cast(_tree)->findEntityByEntityItemID(entityItemID); return loadEntityScript(entity); @@ -156,6 +168,10 @@ QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorTe QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { + if (_shuttingDown) { + return QScriptValue(); // since we're shutting down, we don't load any more scripts + } + if (!entity) { return QScriptValue(); // no entity... } @@ -235,7 +251,7 @@ void EntityTreeRenderer::setTree(Octree* newTree) { } void EntityTreeRenderer::update() { - if (_tree) { + if (_tree && !_shuttingDown) { EntityTree* tree = static_cast(_tree); tree->update(); @@ -258,7 +274,7 @@ void EntityTreeRenderer::update() { } void EntityTreeRenderer::checkEnterLeaveEntities() { - if (_tree) { + if (_tree && !_shuttingDown) { _tree->lockForWrite(); // so that our scripts can do edits if they want glm::vec3 avatarPosition = _viewState->getAvatarPosition() / (float) TREE_SCALE; @@ -309,7 +325,7 @@ void EntityTreeRenderer::checkEnterLeaveEntities() { } void EntityTreeRenderer::leaveAllEntities() { - if (_tree) { + if (_tree && !_shuttingDown) { _tree->lockForWrite(); // so that our scripts can do edits if they want // for all of our previous containing entities, if they are no longer containing then send them a leave event @@ -330,7 +346,7 @@ void EntityTreeRenderer::leaveAllEntities() { } } void EntityTreeRenderer::render(RenderArgs::RenderMode renderMode, RenderArgs::RenderSide renderSide) { - if (_tree) { + if (_tree && !_shuttingDown) { Model::startScene(renderSide); RenderArgs args = { this, _viewFrustum, getSizeScale(), getBoundaryLevelAdjust(), renderMode, renderSide, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; @@ -700,9 +716,14 @@ QScriptValueList EntityTreeRenderer::createEntityArgs(const EntityItemID& entity } void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { + // If we don't have a tree, or we're in the process of shutting down, then don't + // process these events. + if (!_tree || _shuttingDown) { + return; + } PerformanceTimer perfTimer("EntityTreeRenderer::mousePressEvent"); PickRay ray = _viewState->computePickRay(event->x(), event->y()); - + bool precisionPicking = !_dontDoPrecisionPicking; RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::Lock, precisionPicking); if (rayPickResult.intersects) { @@ -714,7 +735,7 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int device if (entityScript.property("mousePressOnEntity").isValid()) { entityScript.property("mousePressOnEntity").call(entityScript, entityScriptArgs); } - + _currentClickingOnEntityID = rayPickResult.entityID; emit clickDownOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID)); if (entityScript.property("clickDownOnEntity").isValid()) { @@ -726,6 +747,11 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int device } void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID) { + // If we don't have a tree, or we're in the process of shutting down, then don't + // process these events. + if (!_tree || _shuttingDown) { + return; + } PerformanceTimer perfTimer("EntityTreeRenderer::mouseReleaseEvent"); PickRay ray = _viewState->computePickRay(event->x(), event->y()); bool precisionPicking = !_dontDoPrecisionPicking; @@ -740,7 +766,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int devi entityScript.property("mouseReleaseOnEntity").call(entityScript, entityScriptArgs); } } - + // Even if we're no longer intersecting with an entity, if we started clicking on it, and now // we're releasing the button, then this is considered a clickOn event if (!_currentClickingOnEntityID.isInvalidID()) { @@ -752,7 +778,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int devi currentClickingEntity.property("clickReleaseOnEntity").call(currentClickingEntity, currentClickingEntityArgs); } } - + // makes it the unknown ID, we just released so we can't be clicking on anything _currentClickingOnEntityID = EntityItemID::createInvalidEntityID(); _lastMouseEvent = MouseEvent(*event, deviceID); @@ -760,10 +786,15 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int devi } void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { + // If we don't have a tree, or we're in the process of shutting down, then don't + // process these events. + if (!_tree || _shuttingDown) { + return; + } PerformanceTimer perfTimer("EntityTreeRenderer::mouseMoveEvent"); PickRay ray = _viewState->computePickRay(event->x(), event->y()); - + bool precisionPicking = false; // for mouse moves we do not do precision picking RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock, precisionPicking); if (rayPickResult.intersects) { @@ -774,15 +805,15 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI if (entityScript.property("mouseMoveEvent").isValid()) { entityScript.property("mouseMoveEvent").call(entityScript, entityScriptArgs); } - + //qDebug() << "mouseMoveEvent over entity:" << rayPickResult.entityID; emit mouseMoveOnEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); if (entityScript.property("mouseMoveOnEntity").isValid()) { entityScript.property("mouseMoveOnEntity").call(entityScript, entityScriptArgs); } - + // handle the hover logic... - + // if we were previously hovering over an entity, and this new entity is not the same as our previous entity // then we need to send the hover leave. if (!_currentHoverOverEntityID.isInvalidID() && rayPickResult.entityID != _currentHoverOverEntityID) { @@ -832,7 +863,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI _currentHoverOverEntityID = EntityItemID::createInvalidEntityID(); // makes it the unknown ID } } - + // Even if we're no longer intersecting with an entity, if we started clicking on an entity and we have // not yet released the hold then this is still considered a holdingClickOnEntity event if (!_currentClickingOnEntityID.isInvalidID()) { @@ -850,29 +881,37 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceI } void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { - checkAndCallUnload(entityID); + if (_tree && !_shuttingDown) { + checkAndCallUnload(entityID); + } _entityScripts.remove(entityID); } void EntityTreeRenderer::entitySciptChanging(const EntityItemID& entityID) { - checkAndCallUnload(entityID); - checkAndCallPreload(entityID); + if (_tree && !_shuttingDown) { + checkAndCallUnload(entityID); + checkAndCallPreload(entityID); + } } void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID) { - // load the entity script if needed... - QScriptValue entityScript = loadEntityScript(entityID); - if (entityScript.property("preload").isValid()) { - QScriptValueList entityArgs = createEntityArgs(entityID); - entityScript.property("preload").call(entityScript, entityArgs); + if (_tree && !_shuttingDown) { + // load the entity script if needed... + QScriptValue entityScript = loadEntityScript(entityID); + if (entityScript.property("preload").isValid()) { + QScriptValueList entityArgs = createEntityArgs(entityID); + entityScript.property("preload").call(entityScript, entityArgs); + } } } void EntityTreeRenderer::checkAndCallUnload(const EntityItemID& entityID) { - QScriptValue entityScript = getPreviouslyLoadedEntityScript(entityID); - if (entityScript.property("unload").isValid()) { - QScriptValueList entityArgs = createEntityArgs(entityID); - entityScript.property("unload").call(entityScript, entityArgs); + if (_tree && !_shuttingDown) { + QScriptValue entityScript = getPreviouslyLoadedEntityScript(entityID); + if (entityScript.property("unload").isValid()) { + QScriptValueList entityArgs = createEntityArgs(entityID); + entityScript.property("unload").call(entityScript, entityArgs); + } } } @@ -887,6 +926,11 @@ void EntityTreeRenderer::changingEntityID(const EntityItemID& oldEntityID, const void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) { + // If we don't have a tree, or we're in the process of shutting down, then don't + // process these events. + if (!_tree || _shuttingDown) { + return; + } QScriptValue entityScriptA = loadEntityScript(idA); if (entityScriptA.property("collisionWithEntity").isValid()) { QScriptValueList args; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 3826a80238..0da85f360b 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -46,6 +46,7 @@ public: virtual int getBoundaryLevelAdjust() const; virtual void setTree(Octree* newTree); + void shutdown(); void update(); EntityTree* getTree() { return static_cast(_tree); } @@ -154,6 +155,8 @@ private: bool _displayModelElementProxy; bool _dontDoPrecisionPicking; + bool _shuttingDown = false; + }; #endif // hifi_EntityTreeRenderer_h diff --git a/libraries/metavoxels/src/Spanner.cpp b/libraries/metavoxels/src/Spanner.cpp index fbd0d311f5..fd5a59fdfb 100644 --- a/libraries/metavoxels/src/Spanner.cpp +++ b/libraries/metavoxels/src/Spanner.cpp @@ -1959,7 +1959,7 @@ HeightfieldNode* HeightfieldNode::fillHeight(const glm::vec3& translation, const } else { colorWidth = innerHeightWidth + HeightfieldData::SHARED_EDGE; colorHeight = innerHeightHeight + HeightfieldData::SHARED_EDGE; - newColorContents = QByteArray(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFF); + newColorContents = QByteArray(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFFu); } int innerColorWidth = colorWidth - HeightfieldData::SHARED_EDGE; int innerColorHeight = colorHeight - HeightfieldData::SHARED_EDGE; @@ -2185,7 +2185,7 @@ HeightfieldNode* HeightfieldNode::setMaterial(const glm::vec3& translation, cons } else { colorWidth = innerHeightWidth + HeightfieldData::SHARED_EDGE; colorHeight = innerHeightHeight + HeightfieldData::SHARED_EDGE; - newColorContents = QByteArray(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFF); + newColorContents = QByteArray(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFFu); } int innerColorWidth = colorWidth - HeightfieldData::SHARED_EDGE; int innerColorHeight = colorHeight - HeightfieldData::SHARED_EDGE; @@ -2428,7 +2428,7 @@ HeightfieldNode* HeightfieldNode::setMaterial(const glm::vec3& translation, cons } } } - bool nextAlphaY = stackDest->getEntryAlpha(y + 1, voxelHeight); + int nextAlphaY = stackDest->getEntryAlpha(y + 1, voxelHeight); if (nextAlphaY == currentAlpha) { entryDest->hermiteY = 0; @@ -2447,7 +2447,7 @@ HeightfieldNode* HeightfieldNode::setMaterial(const glm::vec3& translation, cons } int nextStackZ = (int)stackZ + 1; if (nextStackZ <= innerStackHeight) { - bool nextAlphaZ = newStackContents.at(nextStackZ * stackWidth + (int)stackX).getEntryAlpha( + int nextAlphaZ = newStackContents.at(nextStackZ * stackWidth + (int)stackX).getEntryAlpha( y, nextVoxelHeightZ); if (nextAlphaZ == currentAlpha) { entryDest->hermiteZ = 0; @@ -2828,7 +2828,7 @@ void HeightfieldNode::mergeChildren(bool height, bool colorMaterial) { _height.reset(); } if (colorWidth > 0 && colorMaterial) { - QByteArray colorContents(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFF); + QByteArray colorContents(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFFu); for (int i = 0; i < CHILD_COUNT; i++) { HeightfieldColorPointer childColor = _children[i]->getColor(); if (!childColor) { @@ -3266,7 +3266,7 @@ HeightfieldNode* HeightfieldNode::subdivide(const QVector& heightConten } for (int i = 0; i < CHILD_COUNT; i++) { QVector childHeightContents(heightWidth * heightHeight); - QByteArray childColorContents(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFF); + QByteArray childColorContents(colorWidth * colorHeight * DataBlock::COLOR_BYTES, 0xFFu); QByteArray childMaterialContents(materialWidth * materialHeight, 0); QVector childStackContents(stackWidth * stackHeight); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 0956374238..d201c1568c 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -41,6 +41,7 @@ #include "MIDIEvent.h" + EntityScriptingInterface ScriptEngine::_entityScriptingInterface; static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ @@ -94,8 +95,109 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam _isUserLoaded(false), _arrayBufferClass(new ArrayBufferClass(this)) { + _allScriptsMutex.lock(); + _allKnownScriptEngines.insert(this); + _allScriptsMutex.unlock(); } +ScriptEngine::~ScriptEngine() { + // If we're not already in the middle of stopping all scripts, then we should remove ourselves + // from the list of running scripts. We don't do this if we're in the process of stopping all scripts + // because that method removes scripts from its list as it iterates them + if (!_stoppingAllScripts) { + _allScriptsMutex.lock(); + _allKnownScriptEngines.remove(this); + _allScriptsMutex.unlock(); + } +} + +QSet ScriptEngine::_allKnownScriptEngines; +QMutex ScriptEngine::_allScriptsMutex; +bool ScriptEngine::_stoppingAllScripts = false; +bool ScriptEngine::_doneRunningThisScript = false; + +void ScriptEngine::stopAllScripts(QObject* application) { + _allScriptsMutex.lock(); + _stoppingAllScripts = true; + + QMutableSetIterator i(_allKnownScriptEngines); + while (i.hasNext()) { + ScriptEngine* scriptEngine = i.next(); + + QString scriptName = scriptEngine->getFilename(); + + // NOTE: typically all script engines are running. But there's at least one known exception to this, the + // "entities sandbox" which is only used to evaluate entities scripts to test their validity before using + // them. We don't need to stop scripts that aren't running. + if (scriptEngine->isRunning()) { + + // If the script is running, but still evaluating then we need to wait for its evaluation step to + // complete. After that we can handle the stop process appropriately + if (scriptEngine->evaluatePending()) { + while (scriptEngine->evaluatePending()) { + + // This event loop allows any started, but not yet finished evaluate() calls to complete + // we need to let these complete so that we can be guaranteed that the script engine isn't + // in a partially setup state, which can confuse our shutdown unwinding. + QEventLoop loop; + QObject::connect(scriptEngine, &ScriptEngine::evaluationFinished, &loop, &QEventLoop::quit); + loop.exec(); + } + } + + // We disconnect any script engine signals from the application because we don't want to do any + // extra stopScript/loadScript processing that the Application normally does when scripts start + // and stop. We can safely short circuit this because we know we're in the "quitting" process + scriptEngine->disconnect(application); + + // Calling stop on the script engine will set it's internal _isFinished state to true, and result + // in the ScriptEngine gracefully ending it's run() method. + scriptEngine->stop(); + + // We need to wait for the engine to be done running before we proceed, because we don't + // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing + // any application state after we leave this stopAllScripts() method + scriptEngine->waitTillDoneRunning(); + + // If the script is stopped, we can remove it from our set + i.remove(); + } + } + _stoppingAllScripts = false; + _allScriptsMutex.unlock(); +} + + +void ScriptEngine::waitTillDoneRunning() { + QString scriptName = getFilename(); + + // If the script never started running or finished running before we got here, we don't need to wait for it + if (_isRunning) { + + _doneRunningThisScript = false; // NOTE: this is static, we serialize our waiting for scripts to finish + + // NOTE: waitTillDoneRunning() will be called on the main Application thread, inside of stopAllScripts() + // we want the application thread to continue to process events, because the scripts will likely need to + // marshall messages across to the main thread. For example if they access Settings or Meny in any of their + // shutdown code. + while (!_doneRunningThisScript) { + + // process events for the main application thread, allowing invokeMethod calls to pass between threads + QCoreApplication::processEvents(); + } + } +} + +QString ScriptEngine::getFilename() const { + QStringList fileNameParts = _fileNameString.split("/"); + QString lastPart; + if (!fileNameParts.isEmpty()) { + lastPart = fileNameParts.last(); + } + return lastPart; +} + + void ScriptEngine::setIsAvatar(bool isAvatar) { _isAvatar = isAvatar; @@ -295,12 +397,18 @@ void ScriptEngine::registerGetterSetter(const QString& name, QScriptEngine::Func } void ScriptEngine::evaluate() { + if (_stoppingAllScripts) { + return; // bail early + } + if (!_isInitialized) { init(); } QScriptValue result = evaluate(_scriptContents); + // TODO: why do we check this twice? It seems like the call to clearExcpetions() in the lower level evaluate call + // will cause this code to never actually run... if (hasUncaughtException()) { int line = uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); @@ -310,11 +418,17 @@ void ScriptEngine::evaluate() { } QScriptValue ScriptEngine::evaluate(const QString& program, const QString& fileName, int lineNumber) { + if (_stoppingAllScripts) { + return QScriptValue(); // bail early + } + + _evaluatesPending++; QScriptValue result = QScriptEngine::evaluate(program, fileName, lineNumber); if (hasUncaughtException()) { int line = uncaughtExceptionLineNumber(); qDebug() << "Uncaught exception at (" << _fileNameString << " : " << fileName << ") line" << line << ": " << result.toString(); } + _evaluatesPending--; emit evaluationFinished(result, hasUncaughtException()); clearExceptions(); return result; @@ -333,6 +447,9 @@ void ScriptEngine::sendAvatarBillboardPacket() { } void ScriptEngine::run() { + // TODO: can we add a short circuit for _stoppingAllScripts here? What does it mean to not start running if + // we're in the process of stopping? + if (!_isInitialized) { init(); } @@ -341,12 +458,6 @@ void ScriptEngine::run() { emit runningStateChanged(); QScriptValue result = evaluate(_scriptContents); - if (hasUncaughtException()) { - int line = uncaughtExceptionLineNumber(); - qDebug() << "Uncaught exception at (" << _fileNameString << ") line" << line << ":" << result.toString(); - emit errorMessage("Uncaught exception at (" + _fileNameString + ") line" + QString::number(line) + ":" + result.toString()); - clearExceptions(); - } QElapsedTimer startTime; startTime.start(); @@ -373,7 +484,7 @@ void ScriptEngine::run() { break; } - if (_entityScriptingInterface.getEntityPacketSender()->serversExist()) { + if (!_isFinished && _entityScriptingInterface.getEntityPacketSender()->serversExist()) { // release the queue of edit entity messages. _entityScriptingInterface.getEntityPacketSender()->releaseQueuedMessages(); @@ -383,7 +494,7 @@ void ScriptEngine::run() { } } - if (_isAvatar && _avatarData) { + if (!_isFinished && _isAvatar && _avatarData) { const int SCRIPT_AUDIO_BUFFER_SAMPLES = floor(((SCRIPT_DATA_CALLBACK_USECS * AudioConstants::SAMPLE_RATE) / (1000 * 1000)) + 0.5); @@ -493,9 +604,14 @@ void ScriptEngine::run() { clearExceptions(); } - emit update(deltaTime); + if (!_isFinished) { + emit update(deltaTime); + } lastUpdate = now; + } + + stopAllTimers(); // make sure all our timers are stopped if the script is ending emit scriptEnding(); // kill the avatar identity timer @@ -520,6 +636,21 @@ void ScriptEngine::run() { _isRunning = false; emit runningStateChanged(); + + emit doneRunning(); + + _doneRunningThisScript = true; +} + +// NOTE: This is private because it must be called on the same thread that created the timers, which is why +// we want to only call it in our own run "shutdown" processing. +void ScriptEngine::stopAllTimers() { + QMutableHashIterator i(_timerFunctionMap); + while (i.hasNext()) { + i.next(); + QTimer* timer = i.key(); + stopTimer(timer); + } } void ScriptEngine::stop() { @@ -560,10 +691,20 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int } QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) { + if (_stoppingAllScripts) { + qDebug() << "Script.setInterval() while shutting down is ignored... parent script:" << getFilename(); + return NULL; // bail early + } + return setupTimerWithInterval(function, intervalMS, false); } QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) { + if (_stoppingAllScripts) { + qDebug() << "Script.setTimeout() while shutting down is ignored... parent script:" << getFilename(); + return NULL; // bail early + } + return setupTimerWithInterval(function, timeoutMS, true); } @@ -604,13 +745,16 @@ void ScriptEngine::print(const QString& message) { emit printedMessage(message); } -/** - * If a callback is specified, the included files will be loaded asynchronously and the callback will be called - * when all of the files have finished loading. - * If no callback is specified, the included files will be loaded synchronously and will block execution until - * all of the files have finished loading. - */ +// If a callback is specified, the included files will be loaded asynchronously and the callback will be called +// when all of the files have finished loading. +// If no callback is specified, the included files will be loaded synchronously and will block execution until +// all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { + if (_stoppingAllScripts) { + qDebug() << "Script.include() while shutting down is ignored..." + << "includeFiles:" << includeFiles << "parent script:" << getFilename(); + return; // bail early + } QList urls; for (QString file : includeFiles) { urls.append(resolvePath(file)); @@ -650,12 +794,27 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac } void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { + if (_stoppingAllScripts) { + qDebug() << "Script.include() while shutting down is ignored... " + << "includeFile:" << includeFile << "parent script:" << getFilename(); + return; // bail early + } + QStringList urls; urls.append(includeFile); include(urls, callback); } +// NOTE: The load() command is similar to the include() command except that it loads the script +// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which +// the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { + if (_stoppingAllScripts) { + qDebug() << "Script.load() while shutting down is ignored... " + << "loadFile:" << loadFile << "parent script:" << getFilename(); + return; // bail early + } + QUrl url = resolvePath(loadFile); emit loadScript(url.toString(), false); } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index f2911842e6..154fdb88e3 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -43,6 +44,8 @@ public: const QString& fileNameString = QString(""), AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); + ~ScriptEngine(); + /// Access the EntityScriptingInterface in order to initialize it with a custom packet sender and jurisdiction listener static EntityScriptingInterface* getEntityScriptingInterface() { return &_entityScriptingInterface; } @@ -83,11 +86,18 @@ public: bool isFinished() const { return _isFinished; } bool isRunning() const { return _isRunning; } + bool evaluatePending() const { return _evaluatesPending > 0; } void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; } bool isUserLoaded() const { return _isUserLoaded; } void setParentURL(const QString& parentURL) { _parentURL = parentURL; } + + QString getFilename() const; + + static void stopAllScripts(QObject* application); + + void waitTillDoneRunning(); public slots: void loadURL(const QUrl& scriptURL); @@ -118,12 +128,14 @@ signals: void runningStateChanged(); void evaluationFinished(QScriptValue result, bool isException); void loadScript(const QString& scriptName, bool isUserLoaded); + void doneRunning(); protected: QString _scriptContents; QString _parentURL; bool _isFinished; bool _isRunning; + int _evaluatesPending = 0; bool _isInitialized; bool _isAvatar; QTimer* _avatarIdentityTimer; @@ -134,6 +146,7 @@ protected: int _numAvatarSoundSentBytes; private: + void stopAllTimers(); void sendAvatarIdentityPacket(); void sendAvatarBillboardPacket(); @@ -156,6 +169,12 @@ private: QHash _outgoingScriptAudioSequenceNumbers; private slots: void handleScriptDownload(); + +private: + static QSet _allKnownScriptEngines; + static QMutex _allScriptsMutex; + static bool _stoppingAllScripts; + static bool _doneRunningThisScript; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index aaec8b3e4a..cf15830a10 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -12,8 +12,10 @@ #include #include -#include "SharedUtil.h" #include "GeometryUtil.h" +#include "PlaneShape.h" +#include "RayIntersectionInfo.h" +#include "SharedUtil.h" glm::vec3 computeVectorFromPointToSegment(const glm::vec3& point, const glm::vec3& start, const glm::vec3& end) { // compute the projection of the point vector onto the segment vector @@ -491,3 +493,34 @@ void PolygonClip::copyCleanArray(int& lengthA, glm::vec2* vertexArrayA, int& len } } } + +bool findRayRectangleIntersection(const glm::vec3& origin, const glm::vec3& direction, + const glm::quat& rotation, const glm::vec3& position, const glm::vec2& dimensions) { + RayIntersectionInfo rayInfo; + rayInfo._rayStart = origin; + rayInfo._rayDirection = direction; + rayInfo._rayLength = std::numeric_limits::max(); + + PlaneShape plane; + + const glm::vec3 UNROTATED_NORMAL(0.0f, 0.0f, -1.0f); + glm::vec3 normal = rotation * UNROTATED_NORMAL; + plane.setNormal(normal); + plane.setPoint(position); // the position is definitely a point on our plane + + bool intersects = plane.findRayIntersection(rayInfo); + + if (intersects) { + float distance = rayInfo._hitDistance; + + glm::vec3 hitPosition = origin + (distance * direction); + glm::vec3 localHitPosition = glm::inverse(rotation) * (hitPosition - position); + + glm::vec2 halfDimensions = 0.5f * dimensions; + + intersects = -halfDimensions.x <= localHitPosition.x && localHitPosition.x <= halfDimensions.x + && -halfDimensions.y <= localHitPosition.y && localHitPosition.y <= halfDimensions.y; + } + + return intersects; +} diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index a6889ef73e..e0de536e97 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -76,6 +76,9 @@ bool findRaySphereIntersection(const glm::vec3& origin, const glm::vec3& directi bool findRayCapsuleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& start, const glm::vec3& end, float radius, float& distance); +bool findRayRectangleIntersection(const glm::vec3& origin, const glm::vec3& direction, + const glm::quat& rotation, const glm::vec3& position, const glm::vec2& dimensions); + bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance); diff --git a/tests/jitter/src/main.cpp b/tests/jitter/src/main.cpp index baaa2d08d6..788ae89c6f 100644 --- a/tests/jitter/src/main.cpp +++ b/tests/jitter/src/main.cpp @@ -8,7 +8,7 @@ #include #ifdef _WINDOWS -#include +#include #else #include #include @@ -76,7 +76,7 @@ void runSend(const char* addressOption, int port, int gap, int size, int report) memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; - servaddr.sin_addr.s_addr = inet_addr(addressOption); + inet_pton(AF_INET, addressOption, &servaddr.sin_addr); servaddr.sin_port = htons(port); const int SAMPLES_FOR_SECOND = 1000000 / gap;