diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index e7ac7577b9..929d6c76c8 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -96,17 +96,14 @@ AssignmentClient::AssignmentClient(int &argc, char **argv) : assignmentServerPort = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_PORT_OPTION).toString().toUInt(); } - - HifiSockAddr assignmentServerSocket(DEFAULT_ASSIGNMENT_SERVER_HOSTNAME, assignmentServerPort); // check for an overriden assignment server hostname if (argumentVariantMap.contains(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION)) { - _assignmentServerHostname = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION).toString(); - // change the hostname for our assignment server - assignmentServerSocket = HifiSockAddr(_assignmentServerHostname, assignmentServerSocket.getPort()); + _assignmentServerHostname = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION).toString(); } + HifiSockAddr assignmentServerSocket(_assignmentServerHostname, assignmentServerPort, true); nodeList->setAssignmentServerSocket(assignmentServerSocket); qDebug() << "Assignment server socket is" << assignmentServerSocket; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c6ab0285ea..131aa3fbe3 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -388,7 +388,7 @@ void DomainServer::setupAutomaticNetworking() { const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; QTimer* dataHeartbeatTimer = new QTimer(this); - connect(dataHeartbeatTimer, &QTimer::timeout, this, &DomainServer::sendHeartbeatToDataServer); + connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToDataServer())); dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); } @@ -637,7 +637,7 @@ bool DomainServer::shouldAllowConnectionFromNode(const QString& username, } if (allowedUsers.count() > 0) { - if (allowedUsers.contains(username)) { + if (allowedUsers.contains(username, Qt::CaseInsensitive)) { // it's possible this user can be allowed to connect, but we need to check their username signature QByteArray publicKeyArray = _userPublicKeys.value(username); @@ -657,7 +657,7 @@ bool DomainServer::shouldAllowConnectionFromNode(const QString& username, rsaPublicKey, RSA_PKCS1_PADDING); if (decryptResult != -1) { - if (username == decryptedArray) { + if (username.toLower() == decryptedArray) { qDebug() << "Username signature matches for" << username << "- allowing connection."; // free up the public key before we return @@ -1091,10 +1091,10 @@ QJsonObject jsonForDomainSocketUpdate(const HifiSockAddr& socket) { const QString DOMAIN_UPDATE_AUTOMATIC_NETWORKING_KEY = "automatic_networking"; void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) { - updateDomainInDataServer(newPublicSockAddr.getAddress().toString()); + sendHeartbeatToDataServer(newPublicSockAddr.getAddress().toString()); } -void DomainServer::updateDomainInDataServer(const QString& networkAddress) { +void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) { const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; const QUuid& domainID = LimitedNodeList::getInstance()->getSessionUUID(); @@ -1109,6 +1109,21 @@ void DomainServer::updateDomainInDataServer(const QString& networkAddress) { domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; + // add the number of currently connected agent users + int numConnectedAuthedUsers = 0; + foreach(const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { + if (node->getLinkedData() && !static_cast(node->getLinkedData())->getUsername().isEmpty()) { + ++numConnectedAuthedUsers; + } + } + + const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + const QString HEARTBEAT_NUM_USERS_KEY = "num_users"; + + QJsonObject heartbeatObject; + heartbeatObject[HEARTBEAT_NUM_USERS_KEY] = numConnectedAuthedUsers; + domainObject[DOMAIN_HEARTBEAT_KEY] = heartbeatObject; + QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); AccountManager::getInstance().authenticatedRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index e5f321639c..de485da5e7 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -66,7 +66,7 @@ private slots: void requestCurrentPublicSocketViaSTUN(); void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr); void performICEUpdates(); - void sendHeartbeatToDataServer() { updateDomainInDataServer(); } + void sendHeartbeatToDataServer() { sendHeartbeatToDataServer(QString()); } void sendHeartbeatToIceServer(); void sendICEPingPackets(); private: @@ -77,7 +77,7 @@ private: bool optionallySetupAssignmentPayment(); void setupAutomaticNetworking(); - void updateDomainInDataServer(const QString& networkAddress = QString()); + void sendHeartbeatToDataServer(const QString& networkAddress); void processICEPingReply(const QByteArray& packet, const HifiSockAddr& senderSockAddr); void processICEHeartbeatResponse(const QByteArray& packet); diff --git a/examples/entityScripts/changeColorOnHover.js b/examples/entityScripts/changeColorOnHover.js new file mode 100644 index 0000000000..de3f5f3784 --- /dev/null +++ b/examples/entityScripts/changeColorOnHover.js @@ -0,0 +1,38 @@ +// +// changeColorOnHover.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/1/14. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to a non-model entity like a box or sphere, will +// change the color of the entity when you hover over it. This script uses the JavaScript prototype/class functionality +// to construct an object that has methods for hoverEnterEntity and hoverLeaveEntity; +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function(){ + this.oldColor = {}; + this.oldColorKnown = false; + this.storeOldColor = function(entityID) { + var oldProperties = Entities.getEntityProperties(entityID); + this.oldColor = oldProperties.color; + this.oldColorKnown = true; + print("storing old color... this.oldColor=" + this.oldColor.red + "," + this.oldColor.green + "," + this.oldColor.blue); + }; + this.hoverEnterEntity = function(entityID, mouseEvent) { + if (!this.oldColorKnown) { + this.storeOldColor(entityID); + } + Entities.editEntity(entityID, { color: { red: 0, green: 255, blue: 255} }); + }; + this.hoverLeaveEntity = function(entityID, mouseEvent) { + if (this.oldColorKnown) { + print("leave restoring old color... this.oldColor=" + + this.oldColor.red + "," + this.oldColor.green + "," + this.oldColor.blue); + Entities.editEntity(entityID, { color: this.oldColor }); + } + }; +}) \ No newline at end of file diff --git a/examples/entityScripts/crazylegsOnClick.js b/examples/entityScripts/crazylegsOnClick.js new file mode 100644 index 0000000000..149c8bf43e --- /dev/null +++ b/examples/entityScripts/crazylegsOnClick.js @@ -0,0 +1,57 @@ +// +// crazylegsOnClick.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/3/14. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, that entity will make your avatar do the +// crazyLegs dance if you click on it. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function(){ + var cumulativeTime = 0.0; + var FREQUENCY = 5.0; + var AMPLITUDE = 45.0; + var jointList = MyAvatar.getJointNames(); + var jointMappings = "\n# Joint list start"; + for (var i = 0; i < jointList.length; i++) { + jointMappings = jointMappings + "\njointIndex = " + jointList[i] + " = " + i; + } + print(jointMappings + "\n# Joint list end"); + + this.crazyLegsUpdate = function(deltaTime) { + print("crazyLegsUpdate... deltaTime:" + deltaTime); + cumulativeTime += deltaTime; + print("crazyLegsUpdate... cumulativeTime:" + cumulativeTime); + MyAvatar.setJointData("RightUpLeg", Quat.fromPitchYawRollDegrees(AMPLITUDE * Math.sin(cumulativeTime * FREQUENCY), 0.0, 0.0)); + MyAvatar.setJointData("LeftUpLeg", Quat.fromPitchYawRollDegrees(-AMPLITUDE * Math.sin(cumulativeTime * FREQUENCY), 0.0, 0.0)); + MyAvatar.setJointData("RightLeg", Quat.fromPitchYawRollDegrees( + AMPLITUDE * (1.0 + Math.sin(cumulativeTime * FREQUENCY)),0.0, 0.0)); + MyAvatar.setJointData("LeftLeg", Quat.fromPitchYawRollDegrees( + AMPLITUDE * (1.0 - Math.sin(cumulativeTime * FREQUENCY)),0.0, 0.0)); + }; + + this.stopCrazyLegs = function() { + MyAvatar.clearJointData("RightUpLeg"); + MyAvatar.clearJointData("LeftUpLeg"); + MyAvatar.clearJointData("RightLeg"); + MyAvatar.clearJointData("LeftLeg"); + }; + + this.clickDownOnEntity = function(entityID, mouseEvent) { + print("clickDownOnEntity()..."); + cumulativeTime = 0.0; + Script.update.connect(this.crazyLegsUpdate); + }; + + this.clickReleaseOnEntity = function(entityID, mouseEvent) { + print("clickReleaseOnEntity()..."); + this.stopCrazyLegs(); + Script.update.disconnect(this.crazyLegsUpdate); + }; +}) + diff --git a/examples/entityScripts/playSoundOnClick.js b/examples/entityScripts/playSoundOnClick.js new file mode 100644 index 0000000000..b261bb269a --- /dev/null +++ b/examples/entityScripts/playSoundOnClick.js @@ -0,0 +1,24 @@ +// +// playSoundOnClick.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/3/14. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, that entity will play a sound in world when +// you click on it. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +(function(){ + var bird = new Sound("http://s3.amazonaws.com/hifi-public/sounds/Animals/bushtit_1.raw"); + this.clickDownOnEntity = function(entityID, mouseEvent) { + print("clickDownOnEntity()..."); + var options = new AudioInjectionOptions(); + var position = MyAvatar.position; + options.position = position; + options.volume = 0.5; + Audio.playSound(bird, options); + }; +}) diff --git a/examples/entityScripts/playSoundOnEnterOrLeave.js b/examples/entityScripts/playSoundOnEnterOrLeave.js new file mode 100644 index 0000000000..228a8a36d0 --- /dev/null +++ b/examples/entityScripts/playSoundOnEnterOrLeave.js @@ -0,0 +1,32 @@ +// +// playSoundOnEnterOrLeave.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/3/14. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, that entity will play a sound in world when +// your avatar enters or leaves the bounds of the entity. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +(function(){ + var bird = new Sound("http://s3.amazonaws.com/hifi-public/sounds/Animals/bushtit_1.raw"); + + function playSound(entityID) { + var options = new AudioInjectionOptions(); + var position = MyAvatar.position; + options.position = position; + options.volume = 0.5; + Audio.playSound(bird, options); + }; + + this.enterEntity = function(entityID) { + playSound(); + }; + + this.leaveEntity = function(entityID) { + playSound(); + }; +}) diff --git a/examples/entityScripts/teleportOnClick.js b/examples/entityScripts/teleportOnClick.js new file mode 100644 index 0000000000..11677b12d5 --- /dev/null +++ b/examples/entityScripts/teleportOnClick.js @@ -0,0 +1,18 @@ +// +// teleportOnClick.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 11/3/14. +// Copyright 2014 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, that entity will teleport your avatar if you +// click on it the entity. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +(function(){ + this.clickDownOnEntity = function(entityID, mouseEvent) { + MyAvatar.position = Entities.getEntityProperties(entityID).position; + }; +}) diff --git a/examples/libraries/entityPropertyDialogBox.js b/examples/libraries/entityPropertyDialogBox.js index 2c4bf5329e..01e0c76e0d 100644 --- a/examples/libraries/entityPropertyDialogBox.js +++ b/examples/libraries/entityPropertyDialogBox.js @@ -140,6 +140,9 @@ EntityPropertyDialogBox = (function () { array.push({ label: "Visible:", value: properties.visible }); index++; + + array.push({ label: "Script:", value: properties.script }); + index++; if (properties.type == "Box" || properties.type == "Sphere") { array.push({ label: "Color:", type: "header" }); @@ -282,6 +285,7 @@ EntityPropertyDialogBox = (function () { properties.lifetime = array[index++].value; properties.visible = array[index++].value; + properties.script = array[index++].value; if (properties.type == "Box" || properties.type == "Sphere") { index++; // skip header diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c9a6c50698..b773231b0f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -107,9 +107,6 @@ static unsigned STARFIELD_SEED = 1; static const int BANDWIDTH_METER_CLICK_MAX_DRAG_LENGTH = 6; // farther dragged clicks are ignored -const int IDLE_SIMULATE_MSECS = 16; // How often should call simulate and other stuff - // in the idle loop? (60 FPS is default) - const unsigned MAXIMUM_CACHE_SIZE = 10737418240; // 10GB static QTimer* idleTimer = NULL; @@ -151,7 +148,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _voxelImporter(), _importSucceded(false), _sharedVoxelSystem(TREE_SCALE, DEFAULT_MAX_VOXELS_PER_SYSTEM, &_clipboard), - _entityClipboardRenderer(), + _entities(true), + _entityCollisionSystem(), + _entityClipboardRenderer(false), _entityClipboard(), _wantToKillLocalVoxels(false), _viewFrustum(), @@ -185,9 +184,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _trayIcon(new QSystemTrayIcon(_window)), _lastNackTime(usecTimestampNow()), _lastSendDownstreamAudioStats(usecTimestampNow()), - _renderTargetFramerate(0), - _isVSyncOn(true), - _renderResolutionScale(1.0f) + _isVSyncOn(true) { // read the ApplicationInfo.ini file for Name/Version/Domain information @@ -604,7 +601,7 @@ void Application::paintGL() { if (OculusManager::isConnected()) { _textureCache.setFrameBufferSize(OculusManager::getRenderTargetSize()); } else { - QSize fbSize = _glWidget->getDeviceSize() * _renderResolutionScale; + QSize fbSize = _glWidget->getDeviceSize() * getRenderResolutionScale(); _textureCache.setFrameBufferSize(fbSize); } @@ -1251,6 +1248,8 @@ void Application::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { if (_lastMouseMoveWasSimulated) { showMouse = false; } + + _entities.mouseMoveEvent(event, deviceID); _controllerScriptingInterface.emitMouseMoveEvent(event, deviceID); // send events to any registered scripts @@ -1272,6 +1271,9 @@ void Application::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { } void Application::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { + + _entities.mousePressEvent(event, deviceID); + _controllerScriptingInterface.emitMousePressEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it @@ -1309,6 +1311,9 @@ void Application::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { } void Application::mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID) { + + _entities.mouseReleaseEvent(event, deviceID); + _controllerScriptingInterface.emitMouseReleaseEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it @@ -1474,12 +1479,11 @@ void Application::idle() { bool showWarnings = getLogger()->extraDebugging(); PerformanceWarning warn(showWarnings, "idle()"); - // Only run simulation code if more than IDLE_SIMULATE_MSECS have passed since last time we ran + // Only run simulation code if more than the targetFramePeriod have passed since last time we ran double targetFramePeriod = 0.0; - if (_renderTargetFramerate > 0) { - targetFramePeriod = 1000.0 / _renderTargetFramerate; - } else if (_renderTargetFramerate < 0) { - targetFramePeriod = IDLE_SIMULATE_MSECS; + unsigned int targetFramerate = getRenderTargetFramerate(); + if (targetFramerate > 0) { + targetFramePeriod = 1000.0 / targetFramerate; } double timeSinceLastUpdate = (double)_lastTimeUpdated.nsecsElapsed() / 1000000.0; if (timeSinceLastUpdate > targetFramePeriod) { @@ -1943,6 +1947,10 @@ void Application::init() { connect(&_entityCollisionSystem, &EntityCollisionSystem::entityCollisionWithEntity, ScriptEngine::getEntityScriptingInterface(), &EntityScriptingInterface::entityCollisionWithEntity); + // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing + // of events related clicking, hovering over, and entering entities + _entities.connectSignalsToSlots(ScriptEngine::getEntityScriptingInterface()); + _entityClipboardRenderer.init(); _entityClipboardRenderer.setViewFrustum(getViewFrustum()); _entityClipboardRenderer.setTree(&_entityClipboard); @@ -3179,7 +3187,7 @@ void Application::renderRearViewMirror(const QRect& region, bool billboard) { } else { // if not rendering the billboard, the region is in device independent coordinates; must convert to device QSize size = getTextureCache()->getFrameBufferSize(); - float ratio = QApplication::desktop()->windowHandle()->devicePixelRatio() * _renderResolutionScale; + float ratio = QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); int x = region.x() * ratio, y = region.y() * ratio, width = region.width() * ratio, height = region.height() * ratio; glViewport(x, size.height() - y - height, width, height); glScissor(x, size.height() - y - height, width, height); @@ -3822,35 +3830,7 @@ void joystickFromScriptValue(const QScriptValue &object, Joystick* &out) { out = qobject_cast(object.toQObject()); } -ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUserLoaded, - bool loadScriptFromEditor, bool activateMainWindow) { - QUrl scriptUrl(scriptFilename); - const QString& scriptURLString = scriptUrl.toString(); - if (_scriptEnginesHash.contains(scriptURLString) && loadScriptFromEditor - && !_scriptEnginesHash[scriptURLString]->isFinished()) { - - return _scriptEnginesHash[scriptURLString]; - } - - ScriptEngine* scriptEngine; - if (scriptFilename.isNull()) { - scriptEngine = new ScriptEngine(NO_SCRIPT, "", &_controllerScriptingInterface); - } else { - // start the script on a new thread... - scriptEngine = new ScriptEngine(scriptUrl, &_controllerScriptingInterface); - - if (!scriptEngine->hasScript()) { - qDebug() << "Application::loadScript(), script failed to load..."; - QMessageBox::warning(getWindow(), "Error Loading Script", scriptURLString + " failed to load."); - return NULL; - } - - _scriptEnginesHash.insertMulti(scriptURLString, scriptEngine); - _runningScriptsWidget->setRunningScripts(getRunningScripts()); - UserActivityLogger::getInstance().loadedScript(scriptURLString); - } - scriptEngine->setUserLoaded(isUserLoaded); - +void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine) { // setup the packet senders and jurisdiction listeners of the script engine's scripting interfaces so // we can use the same ones from the application. scriptEngine->getVoxelsScriptingInterface()->setPacketSender(&_voxelEditSender); @@ -3932,6 +3912,38 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser // Starts an event loop, and emits workerThread->started() workerThread->start(); +} + +ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUserLoaded, + bool loadScriptFromEditor, bool activateMainWindow) { + QUrl scriptUrl(scriptFilename); + const QString& scriptURLString = scriptUrl.toString(); + if (_scriptEnginesHash.contains(scriptURLString) && loadScriptFromEditor + && !_scriptEnginesHash[scriptURLString]->isFinished()) { + + return _scriptEnginesHash[scriptURLString]; + } + + ScriptEngine* scriptEngine; + if (scriptFilename.isNull()) { + scriptEngine = new ScriptEngine(NO_SCRIPT, "", &_controllerScriptingInterface); + } else { + // start the script on a new thread... + scriptEngine = new ScriptEngine(scriptUrl, &_controllerScriptingInterface); + + if (!scriptEngine->hasScript()) { + qDebug() << "Application::loadScript(), script failed to load..."; + QMessageBox::warning(getWindow(), "Error Loading Script", scriptURLString + " failed to load."); + return NULL; + } + + _scriptEnginesHash.insertMulti(scriptURLString, scriptEngine); + _runningScriptsWidget->setRunningScripts(getRunningScripts()); + UserActivityLogger::getInstance().loadedScript(scriptURLString); + } + scriptEngine->setUserLoaded(isUserLoaded); + + registerScriptEngineWithApplicationServices(scriptEngine); // restore the main window's active state if (activateMainWindow && !loadScriptFromEditor) { @@ -4237,37 +4249,56 @@ void Application::takeSnapshot() { _snapshotShareDialog->show(); } -void Application::setRenderTargetFramerate(unsigned int framerate, bool vsyncOn) { - if (vsyncOn != _isVSyncOn) { +void Application::setVSyncEnabled(bool vsyncOn) { #if defined(Q_OS_WIN) - if (wglewGetExtension("WGL_EXT_swap_control")) { - wglSwapIntervalEXT(vsyncOn); - int swapInterval = wglGetSwapIntervalEXT(); - _isVSyncOn = swapInterval; - qDebug("V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); - } else { - qDebug("V-Sync is FORCED ON on this system\n"); - } -#elif defined(Q_OS_LINUX) - // TODO: write the poper code for linux - /* - if (glQueryExtension.... ("GLX_EXT_swap_control")) { - glxSwapIntervalEXT(vsyncOn); - int swapInterval = xglGetSwapIntervalEXT(); - _isVSyncOn = swapInterval; - qDebug("V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); - } else { + if (wglewGetExtension("WGL_EXT_swap_control")) { + wglSwapIntervalEXT(vsyncOn); + int swapInterval = wglGetSwapIntervalEXT(); + qDebug("V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); + } else { qDebug("V-Sync is FORCED ON on this system\n"); - } - */ -#else - qDebug("V-Sync is FORCED ON on this system\n"); -#endif } - _renderTargetFramerate = framerate; +#elif defined(Q_OS_LINUX) + // TODO: write the poper code for linux + /* + if (glQueryExtension.... ("GLX_EXT_swap_control")) { + glxSwapIntervalEXT(vsyncOn); + int swapInterval = xglGetSwapIntervalEXT(); + _isVSyncOn = swapInterval; + qDebug("V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF")); + } else { + qDebug("V-Sync is FORCED ON on this system\n"); + } + */ +#else + qDebug("V-Sync is FORCED ON on this system\n"); +#endif } -bool Application::isVSyncEditable() { +bool Application::isVSyncOn() const { +#if defined(Q_OS_WIN) + if (wglewGetExtension("WGL_EXT_swap_control")) { + int swapInterval = wglGetSwapIntervalEXT(); + return (swapInterval > 0); + } else { + return true; + } +#elif defined(Q_OS_LINUX) + // TODO: write the poper code for linux + /* + if (glQueryExtension.... ("GLX_EXT_swap_control")) { + int swapInterval = xglGetSwapIntervalEXT(); + return (swapInterval > 0); + } else { + return true; + } + */ +#else + return true; +#endif +} + +bool Application::isVSyncEditable() const { #if defined(Q_OS_WIN) if (wglewGetExtension("WGL_EXT_swap_control")) { return true; @@ -4284,6 +4315,33 @@ bool Application::isVSyncEditable() { return false; } -void Application::setRenderResolutionScale(float scale) { - _renderResolutionScale = scale; +unsigned int Application::getRenderTargetFramerate() const { + if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerateUnlimited)) { + return 0; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate60)) { + return 60; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate50)) { + return 50; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate40)) { + return 40; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderTargetFramerate30)) { + return 30; + } + return 0; +} + +float Application::getRenderResolutionScale() const { + if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionOne)) { + return 1.f; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { + return 0.666f; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionHalf)) { + return 0.5f; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionThird)) { + return 0.333f; + } else if (Menu::getInstance()->isOptionChecked(MenuOption::RenderResolutionQuarter)) { + return 0.25f; + } else { + return 1.f; + } } diff --git a/interface/src/Application.h b/interface/src/Application.h index 3d2fab07bf..9fcf53cfa9 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -303,7 +303,14 @@ public: bool isLookingAtMyAvatar(Avatar* avatar); - float getRenderResolutionScale() const { return _renderResolutionScale; } + float getRenderResolutionScale() const; + + unsigned int getRenderTargetFramerate() const; + bool isVSyncOn() const; + bool isVSyncEditable() const; + + + void registerScriptEngineWithApplicationServices(ScriptEngine* scriptEngine); signals: @@ -367,12 +374,7 @@ public slots: void domainSettingsReceived(const QJsonObject& domainSettingsObject); - void setRenderTargetFramerate(unsigned int framerate, bool vsyncOn = true); - bool isVSyncOn() { return _isVSyncOn; } - bool isVSyncEditable(); - unsigned int getRenderTargetFramerate() const { return _renderTargetFramerate; } - - void setRenderResolutionScale(float scale); + void setVSyncEnabled(bool vsyncOn); void resetSensors(); @@ -624,9 +626,7 @@ private: quint64 _lastNackTime; quint64 _lastSendDownstreamAudioStats; - int _renderTargetFramerate; bool _isVSyncOn; - float _renderResolutionScale; }; #endif // hifi_Application_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f915855758..21c1cded88 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -377,13 +377,12 @@ Menu::Menu() : { QMenu* framerateMenu = renderOptionsMenu->addMenu(MenuOption::RenderTargetFramerate); QActionGroup* framerateGroup = new QActionGroup(framerateMenu); - + framerateGroup->setExclusive(true); framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerateUnlimited, 0, true)); framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate60, 0, false)); framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate50, 0, false)); framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate40, 0, false)); framerateGroup->addAction(addCheckableActionToQMenuAndActionHash(framerateMenu, MenuOption::RenderTargetFramerate30, 0, false)); - connect(framerateMenu, SIGNAL(triggered(QAction*)), this, SLOT(changeRenderTargetFramerate(QAction*))); #if defined(Q_OS_MAC) #else @@ -394,12 +393,12 @@ Menu::Menu() : QMenu* resolutionMenu = renderOptionsMenu->addMenu(MenuOption::RenderResolution); QActionGroup* resolutionGroup = new QActionGroup(resolutionMenu); - resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionOne, 0, false)); + resolutionGroup->setExclusive(true); + resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionOne, 0, true)); resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionTwoThird, 0, false)); resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionHalf, 0, false)); - resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionThird, 0, true)); + resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionThird, 0, false)); resolutionGroup->addAction(addCheckableActionToQMenuAndActionHash(resolutionMenu, MenuOption::RenderResolutionQuarter, 0, false)); - connect(resolutionMenu, SIGNAL(triggered(QAction*)), this, SLOT(changeRenderResolution(QAction*))); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Stars, Qt::Key_Asterisk, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, @@ -1261,46 +1260,7 @@ void Menu::muteEnvironment() { } void Menu::changeVSync() { - Application::getInstance()->setRenderTargetFramerate( - Application::getInstance()->getRenderTargetFramerate(), - isOptionChecked(MenuOption::RenderTargetFramerateVSyncOn)); -} -void Menu::changeRenderTargetFramerate(QAction* action) { - bool vsynOn = Application::getInstance()->isVSyncOn(); - - QString text = action->text(); - if (text == MenuOption::RenderTargetFramerateUnlimited) { - Application::getInstance()->setRenderTargetFramerate(0, vsynOn); - } - else if (text == MenuOption::RenderTargetFramerate60) { - Application::getInstance()->setRenderTargetFramerate(60, vsynOn); - } - else if (text == MenuOption::RenderTargetFramerate50) { - Application::getInstance()->setRenderTargetFramerate(50, vsynOn); - } - else if (text == MenuOption::RenderTargetFramerate40) { - Application::getInstance()->setRenderTargetFramerate(40, vsynOn); - } - else if (text == MenuOption::RenderTargetFramerate30) { - Application::getInstance()->setRenderTargetFramerate(30, vsynOn); - } -} - -void Menu::changeRenderResolution(QAction* action) { - QString text = action->text(); - if (text == MenuOption::RenderResolutionOne) { - Application::getInstance()->setRenderResolutionScale(1.f); - } else if (text == MenuOption::RenderResolutionTwoThird) { - Application::getInstance()->setRenderResolutionScale(0.666f); - } else if (text == MenuOption::RenderResolutionHalf) { - Application::getInstance()->setRenderResolutionScale(0.5f); - } else if (text == MenuOption::RenderResolutionThird) { - Application::getInstance()->setRenderResolutionScale(0.333f); - } else if (text == MenuOption::RenderResolutionQuarter) { - Application::getInstance()->setRenderResolutionScale(0.25f); - } else { - Application::getInstance()->setRenderResolutionScale(1.f); - } + Application::getInstance()->setVSyncEnabled(isOptionChecked(MenuOption::RenderTargetFramerateVSyncOn)); } void Menu::displayNameLocationResponse(const QString& errorString) { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 72b43e09c8..8af91ccae4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -230,9 +230,7 @@ private slots: void displayAddressOfflineMessage(); void displayAddressNotFoundMessage(); void muteEnvironment(); - void changeRenderTargetFramerate(QAction* action); void changeVSync(); - void changeRenderResolution(QAction* action); private: static Menu* _instance; diff --git a/interface/src/entities/EntityTreeRenderer.cpp b/interface/src/entities/EntityTreeRenderer.cpp index e6b640011d..e447c703fb 100644 --- a/interface/src/entities/EntityTreeRenderer.cpp +++ b/interface/src/entities/EntityTreeRenderer.cpp @@ -11,19 +11,25 @@ #include +#include + #include #include "InterfaceConfig.h" #include #include +#include #include #include #include "Menu.h" +#include "NetworkAccessManager.h" #include "EntityTreeRenderer.h" +#include "devices/OculusManager.h" + #include "RenderableBoxEntityItem.h" #include "RenderableLightEntityItem.h" #include "RenderableModelEntityItem.h" @@ -36,15 +42,21 @@ QThread* EntityTreeRenderer::getMainThread() { -EntityTreeRenderer::EntityTreeRenderer() : - OctreeRenderer() { +EntityTreeRenderer::EntityTreeRenderer(bool wantScripts) : + OctreeRenderer(), + _wantScripts(wantScripts), + _entitiesScriptEngine(NULL) { REGISTER_ENTITY_TYPE_WITH_FACTORY(Model, RenderableModelEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Box, RenderableBoxEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Sphere, RenderableSphereEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Light, RenderableLightEntityItem::factory) + + _currentHoverOverEntityID = EntityItemID::createInvalidEntityID(); // makes it the unknown ID + _currentClickingOnEntityID = EntityItemID::createInvalidEntityID(); // makes it the unknown ID } EntityTreeRenderer::~EntityTreeRenderer() { + // do we need to delete the _entitiesScriptEngine?? or is it deleted by default } void EntityTreeRenderer::clear() { @@ -54,6 +66,117 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::init() { OctreeRenderer::init(); static_cast(_tree)->setFBXService(this); + + if (_wantScripts) { + _entitiesScriptEngine = new ScriptEngine(NO_SCRIPT, "Entities", + Application::getInstance()->getControllerScriptingInterface()); + Application::getInstance()->registerScriptEngineWithApplicationServices(_entitiesScriptEngine); + } + + // make sure our "last avatar position" is something other than our current position, so that on our + // first chance, we'll check for enter/leave entity events. + glm::vec3 avatarPosition = Application::getInstance()->getAvatar()->getPosition(); + _lastAvatarPosition = avatarPosition + glm::vec3(1.f, 1.f, 1.f); +} + +QScriptValue EntityTreeRenderer::loadEntityScript(const EntityItemID& entityItemID) { + EntityItem* entity = static_cast(_tree)->findEntityByEntityItemID(entityItemID); + return loadEntityScript(entity); +} + + +QString EntityTreeRenderer::loadScriptContents(const QString& scriptMaybeURLorText) { + QUrl url(scriptMaybeURLorText); + + // If the url is not valid, this must be script text... + if (!url.isValid()) { + return scriptMaybeURLorText; + } + + QString scriptContents; // assume empty + + // if the scheme length is one or lower, maybe they typed in a file, let's try + const int WINDOWS_DRIVE_LETTER_SIZE = 1; + if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { + url = QUrl::fromLocalFile(scriptMaybeURLorText); + } + + // ok, let's see if it's valid... and if so, load it + if (url.isValid()) { + if (url.scheme() == "file") { + QString fileName = url.toLocalFile(); + QFile scriptFile(fileName); + if (scriptFile.open(QFile::ReadOnly | QFile::Text)) { + qDebug() << "Loading file:" << fileName; + QTextStream in(&scriptFile); + scriptContents = in.readAll(); + } else { + qDebug() << "ERROR Loading file:" << fileName; + } + } else { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* reply = networkAccessManager.get(QNetworkRequest(url)); + qDebug() << "Downloading script at" << url; + QEventLoop loop; + QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { + scriptContents = reply->readAll(); + } else { + qDebug() << "ERROR Loading file:" << url.toString(); + } + } + } + + return scriptContents; +} + + +QScriptValue EntityTreeRenderer::loadEntityScript(EntityItem* entity) { + if (!entity) { + return QScriptValue(); // no entity... + } + + EntityItemID entityID = entity->getEntityItemID(); + if (_entityScripts.contains(entityID)) { + EntityScriptDetails details = _entityScripts[entityID]; + + // check to make sure our script text hasn't changed on us since we last loaded it + if (details.scriptText == entity->getScript()) { + return details.scriptObject; // previously loaded + } + + // if we got here, then we previously loaded a script, but the entity's script value + // has changed and so we need to reload it. + _entityScripts.remove(entityID); + } + if (entity->getScript().isEmpty()) { + return QScriptValue(); // no script + } + + QString scriptContents = loadScriptContents(entity->getScript()); + + if (QScriptEngine::checkSyntax(scriptContents).state() != QScriptSyntaxCheckResult::Valid) { + qDebug() << "EntityTreeRenderer::loadEntityScript() entity:" << entityID; + qDebug() << " INVALID SYNTAX"; + qDebug() << " SCRIPT:" << scriptContents; + return QScriptValue(); // invalid script + } + + QScriptValue entityScriptConstructor = _entitiesScriptEngine->evaluate(scriptContents); + + if (!entityScriptConstructor.isFunction()) { + qDebug() << "EntityTreeRenderer::loadEntityScript() entity:" << entityID; + qDebug() << " NOT CONSTRUCTOR"; + qDebug() << " SCRIPT:" << scriptContents; + return QScriptValue(); // invalid script + } + + QScriptValue entityScriptObject = entityScriptConstructor.construct(); + EntityScriptDetails newDetails = { entity->getScript(), entityScriptObject }; + _entityScripts[entityID] = newDetails; + + return entityScriptObject; // newly constructed } void EntityTreeRenderer::setTree(Octree* newTree) { @@ -65,6 +188,58 @@ void EntityTreeRenderer::update() { if (_tree) { EntityTree* tree = static_cast(_tree); tree->update(); + + // check to see if the avatar has moved and if we need to handle enter/leave entity logic + checkEnterLeaveEntities(); + } +} + +void EntityTreeRenderer::checkEnterLeaveEntities() { + if (_tree) { + glm::vec3 avatarPosition = Application::getInstance()->getAvatar()->getPosition() / (float) TREE_SCALE; + + if (avatarPosition != _lastAvatarPosition) { + float radius = 1.0f / (float) TREE_SCALE; // for now, assume 1 meter radius + QVector foundEntities; + QVector entitiesContainingAvatar; + + // find the entities near us + static_cast(_tree)->findEntities(avatarPosition, radius, foundEntities); + + // create a list of entities that actually contain the avatar's position + foreach(const EntityItem* entity, foundEntities) { + if (entity->contains(avatarPosition)) { + entitiesContainingAvatar << entity->getEntityItemID(); + } + } + + // for all of our previous containing entities, if they are no longer containing then send them a leave event + foreach(const EntityItemID& entityID, _currentEntitiesInside) { + if (!entitiesContainingAvatar.contains(entityID)) { + emit leaveEntity(entityID); + QScriptValueList entityArgs = createEntityArgs(entityID); + QScriptValue entityScript = loadEntityScript(entityID); + if (entityScript.property("leaveEntity").isValid()) { + entityScript.property("leaveEntity").call(entityScript, entityArgs); + } + + } + } + + // for all of our new containing entities, if they weren't previously containing then send them an enter event + foreach(const EntityItemID& entityID, entitiesContainingAvatar) { + if (!_currentEntitiesInside.contains(entityID)) { + emit enterEntity(entityID); + QScriptValueList entityArgs = createEntityArgs(entityID); + QScriptValue entityScript = loadEntityScript(entityID); + if (entityScript.property("enterEntity").isValid()) { + entityScript.property("enterEntity").call(entityScript, entityArgs); + } + } + } + _currentEntitiesInside = entitiesContainingAvatar; + _lastAvatarPosition = avatarPosition; + } } } @@ -101,8 +276,8 @@ const Model* EntityTreeRenderer::getModelForEntityItem(const EntityItem* entityI } void renderElementProxy(EntityTreeElement* entityTreeElement) { - glm::vec3 elementCenter = entityTreeElement->getAACube().calcCenter() * (float)TREE_SCALE; - float elementSize = entityTreeElement->getScale() * (float)TREE_SCALE; + glm::vec3 elementCenter = entityTreeElement->getAACube().calcCenter() * (float) TREE_SCALE; + float elementSize = entityTreeElement->getScale() * (float) TREE_SCALE; glColor3f(1.0f, 0.0f, 0.0f); glPushMatrix(); glTranslatef(elementCenter.x, elementCenter.y, elementCenter.z); @@ -175,9 +350,9 @@ void EntityTreeRenderer::renderProxies(const EntityItem* entity, RenderArgs* arg AACube minCube = entity->getMinimumAACube(); AABox entityBox = entity->getAABox(); - maxCube.scale((float)TREE_SCALE); - minCube.scale((float)TREE_SCALE); - entityBox.scale((float)TREE_SCALE); + maxCube.scale((float) TREE_SCALE); + minCube.scale((float) TREE_SCALE); + entityBox.scale((float) TREE_SCALE); glm::vec3 maxCenter = maxCube.calcCenter(); glm::vec3 minCenter = minCube.calcCenter(); @@ -207,9 +382,9 @@ void EntityTreeRenderer::renderProxies(const EntityItem* entity, RenderArgs* arg glPopMatrix(); - glm::vec3 position = entity->getPosition() * (float)TREE_SCALE; - glm::vec3 center = entity->getCenter() * (float)TREE_SCALE; - glm::vec3 dimensions = entity->getDimensions() * (float)TREE_SCALE; + glm::vec3 position = entity->getPosition() * (float) TREE_SCALE; + glm::vec3 center = entity->getCenter() * (float) TREE_SCALE; + glm::vec3 dimensions = entity->getDimensions() * (float) TREE_SCALE; glm::quat rotation = entity->getRotation(); glColor4f(1.0f, 0.0f, 1.0f, 1.0f); @@ -382,4 +557,207 @@ void EntityTreeRenderer::deleteReleasedModels() { } } +PickRay EntityTreeRenderer::computePickRay(float x, float y) { + float screenWidth = Application::getInstance()->getGLWidget()->width(); + float screenHeight = Application::getInstance()->getGLWidget()->height(); + PickRay result; + if (OculusManager::isConnected()) { + Camera* camera = Application::getInstance()->getCamera(); + result.origin = camera->getPosition(); + Application::getInstance()->getApplicationOverlay().computeOculusPickRay(x / screenWidth, y / screenHeight, result.direction); + } else { + ViewFrustum* viewFrustum = Application::getInstance()->getViewFrustum(); + viewFrustum->computePickRay(x / screenWidth, y / screenHeight, result.origin, result.direction); + } + return result; +} + + +RayToEntityIntersectionResult EntityTreeRenderer::findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType) { + RayToEntityIntersectionResult result; + if (_tree) { + EntityTree* entityTree = static_cast(_tree); + + OctreeElement* element; + EntityItem* intersectedEntity = NULL; + result.intersects = entityTree->findRayIntersection(ray.origin, ray.direction, element, result.distance, result.face, + (void**)&intersectedEntity, lockType, &result.accurate); + if (result.intersects && intersectedEntity) { + result.entityID = intersectedEntity->getEntityItemID(); + result.properties = intersectedEntity->getProperties(); + result.intersection = ray.origin + (ray.direction * result.distance); + result.entity = intersectedEntity; + } + } + return result; +} + +void EntityTreeRenderer::connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface) { + connect(this, &EntityTreeRenderer::mousePressOnEntity, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); + connect(this, &EntityTreeRenderer::mouseMoveOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); + connect(this, &EntityTreeRenderer::mouseReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); + + connect(this, &EntityTreeRenderer::clickDownOnEntity, entityScriptingInterface, &EntityScriptingInterface::clickDownOnEntity); + connect(this, &EntityTreeRenderer::holdingClickOnEntity, entityScriptingInterface, &EntityScriptingInterface::holdingClickOnEntity); + connect(this, &EntityTreeRenderer::clickReleaseOnEntity, entityScriptingInterface, &EntityScriptingInterface::clickReleaseOnEntity); + + connect(this, &EntityTreeRenderer::hoverEnterEntity, entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity); + connect(this, &EntityTreeRenderer::hoverOverEntity, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(this, &EntityTreeRenderer::hoverLeaveEntity, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + + connect(this, &EntityTreeRenderer::enterEntity, entityScriptingInterface, &EntityScriptingInterface::enterEntity); + connect(this, &EntityTreeRenderer::leaveEntity, entityScriptingInterface, &EntityScriptingInterface::leaveEntity); +} + +QScriptValueList EntityTreeRenderer::createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID) { + QScriptValueList args; + args << entityID.toScriptValue(_entitiesScriptEngine); + args << MouseEvent(*event, deviceID).toScriptValue(_entitiesScriptEngine); + return args; +} + +QScriptValueList EntityTreeRenderer::createEntityArgs(const EntityItemID& entityID) { + QScriptValueList args; + args << entityID.toScriptValue(_entitiesScriptEngine); + return args; +} + +void EntityTreeRenderer::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { + PerformanceTimer perfTimer("EntityTreeRenderer::mousePressEvent"); + PickRay ray = computePickRay(event->x(), event->y()); + RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::Lock); + if (rayPickResult.intersects) { + //qDebug() << "mousePressEvent over entity:" << rayPickResult.entityID; + emit mousePressOnEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); + + QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID); + QScriptValue entityScript = loadEntityScript(rayPickResult.entity); + 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()) { + entityScript.property("clickDownOnEntity").call(entityScript, entityScriptArgs); + } + } +} + +void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID) { + PerformanceTimer perfTimer("EntityTreeRenderer::mouseReleaseEvent"); + PickRay ray = computePickRay(event->x(), event->y()); + RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::Lock); + if (rayPickResult.intersects) { + //qDebug() << "mouseReleaseEvent over entity:" << rayPickResult.entityID; + emit mouseReleaseOnEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); + + QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID); + QScriptValue entityScript = loadEntityScript(rayPickResult.entity); + if (entityScript.property("mouseReleaseOnEntity").isValid()) { + 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()) { + emit clickReleaseOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID)); + + QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, event, deviceID); + QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID); + if (currentClickingEntity.property("clickReleaseOnEntity").isValid()) { + 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(); +} + +void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { + PerformanceTimer perfTimer("EntityTreeRenderer::mouseMoveEvent"); + PickRay ray = computePickRay(event->x(), event->y()); + RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock); + if (rayPickResult.intersects) { + QScriptValueList entityScriptArgs = createMouseEventArgs(rayPickResult.entityID, event, deviceID); + + // load the entity script if needed... + QScriptValue entityScript = loadEntityScript(rayPickResult.entity); + 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) { + emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event, deviceID)); + + QScriptValueList currentHoverEntityArgs = createMouseEventArgs(_currentHoverOverEntityID, event, deviceID); + + QScriptValue currentHoverEntity = loadEntityScript(_currentHoverOverEntityID); + if (currentHoverEntity.property("hoverLeaveEntity").isValid()) { + currentHoverEntity.property("hoverLeaveEntity").call(currentHoverEntity, currentHoverEntityArgs); + } + } + + // If the new hover entity does not match the previous hover entity then we are entering the new one + // this is true if the _currentHoverOverEntityID is known or unknown + if (rayPickResult.entityID != _currentHoverOverEntityID) { + emit hoverEnterEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); + if (entityScript.property("hoverEnterEntity").isValid()) { + entityScript.property("hoverEnterEntity").call(entityScript, entityScriptArgs); + } + } + + // and finally, no matter what, if we're intersecting an entity then we're definitely hovering over it, and + // we should send our hover over event + emit hoverOverEntity(rayPickResult.entityID, MouseEvent(*event, deviceID)); + if (entityScript.property("hoverOverEntity").isValid()) { + entityScript.property("hoverOverEntity").call(entityScript, entityScriptArgs); + } + + // remember what we're hovering over + _currentHoverOverEntityID = rayPickResult.entityID; + + } else { + // handle the hover logic... + // if we were previously hovering over an entity, and we're no longer hovering over any entity then we need to + // send the hover leave for our previous entity + if (!_currentHoverOverEntityID.isInvalidID()) { + emit hoverLeaveEntity(_currentHoverOverEntityID, MouseEvent(*event, deviceID)); + + QScriptValueList currentHoverEntityArgs = createMouseEventArgs(_currentHoverOverEntityID, event, deviceID); + + QScriptValue currentHoverEntity = loadEntityScript(_currentHoverOverEntityID); + if (currentHoverEntity.property("hoverLeaveEntity").isValid()) { + currentHoverEntity.property("hoverLeaveEntity").call(currentHoverEntity, currentHoverEntityArgs); + } + + _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()) { + emit holdingClickOnEntity(_currentClickingOnEntityID, MouseEvent(*event, deviceID)); + + QScriptValueList currentClickingEntityArgs = createMouseEventArgs(_currentClickingOnEntityID, event, deviceID); + + QScriptValue currentClickingEntity = loadEntityScript(_currentClickingOnEntityID); + if (currentClickingEntity.property("holdingClickOnEntity").isValid()) { + currentClickingEntity.property("holdingClickOnEntity").call(currentClickingEntity, currentClickingEntityArgs); + } + } +} + diff --git a/interface/src/entities/EntityTreeRenderer.h b/interface/src/entities/EntityTreeRenderer.h index 09d931541d..0a725c8294 100644 --- a/interface/src/entities/EntityTreeRenderer.h +++ b/interface/src/entities/EntityTreeRenderer.h @@ -16,6 +16,7 @@ #include #include +#include // for RayToEntityIntersectionResult #include #include #include @@ -26,11 +27,17 @@ #include "renderer/Model.h" +class EntityScriptDetails { +public: + QString scriptText; + QScriptValue scriptObject; +}; + // Generic client side Octree renderer class. class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService { Q_OBJECT public: - EntityTreeRenderer(); + EntityTreeRenderer(bool wantScripts); virtual ~EntityTreeRenderer(); virtual Octree* createTree() { return new EntityTree(true); } @@ -75,9 +82,55 @@ public: void releaseModel(Model* model); void deleteReleasedModels(); + + // event handles which may generate entity related events + void mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID); + void mousePressEvent(QMouseEvent* event, unsigned int deviceID); + void mouseMoveEvent(QMouseEvent* event, unsigned int deviceID); + + /// connect our signals to anEntityScriptingInterface for firing of events related clicking, + /// hovering over, and entering entities + void connectSignalsToSlots(EntityScriptingInterface* entityScriptingInterface); + +signals: + void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void mouseReleaseOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + + void clickDownOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void holdingClickOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void clickReleaseOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + + void hoverEnterEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void hoverOverEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void hoverLeaveEntity(const EntityItemID& entityItemID, const MouseEvent& event); + + void enterEntity(const EntityItemID& entityItemID); + void leaveEntity(const EntityItemID& entityItemID); + private: QList _releasedModels; void renderProxies(const EntityItem* entity, RenderArgs* args); + PickRay computePickRay(float x, float y); + RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType); + + EntityItemID _currentHoverOverEntityID; + EntityItemID _currentClickingOnEntityID; + + QScriptValueList createEntityArgs(const EntityItemID& entityID); + void checkEnterLeaveEntities(); + glm::vec3 _lastAvatarPosition; + QVector _currentEntitiesInside; + + bool _wantScripts; + ScriptEngine* _entitiesScriptEngine; + + QScriptValue loadEntityScript(EntityItem* entity); + QScriptValue loadEntityScript(const EntityItemID& entityItemID); + QString loadScriptContents(const QString& scriptMaybeURLorText); + QScriptValueList createMouseEventArgs(const EntityItemID& entityID, QMouseEvent* event, unsigned int deviceID); + + QHash _entityScripts; }; #endif // hifi_EntityTreeRenderer_h diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 65da3c964a..2117ec25b0 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -253,6 +253,7 @@ public: void applyHardCollision(const CollisionInfo& collisionInfo); virtual const Shape& getCollisionShapeInMeters() const { return _collisionShape; } + virtual bool contains(const glm::vec3& point) const { return getAABox().contains(point); } protected: virtual void initFromEntityItemID(const EntityItemID& entityItemID); // maybe useful to allow subclasses to init diff --git a/libraries/entities/src/EntityItemID.cpp b/libraries/entities/src/EntityItemID.cpp index c730e44322..aaf6e33128 100644 --- a/libraries/entities/src/EntityItemID.cpp +++ b/libraries/entities/src/EntityItemID.cpp @@ -124,6 +124,10 @@ void EntityItemID::handleAddEntityResponse(const QByteArray& packet) { _tokenIDsToIDs[creatorTokenID] = entityID; } +QScriptValue EntityItemID::toScriptValue(QScriptEngine* engine) const { + return EntityItemIDtoScriptValue(engine, *this); +} + QScriptValue EntityItemIDtoScriptValue(QScriptEngine* engine, const EntityItemID& id) { QScriptValue obj = engine->newObject(); obj.setProperty("id", id.id.toString()); diff --git a/libraries/entities/src/EntityItemID.h b/libraries/entities/src/EntityItemID.h index 7a3f822c7c..1ac8a67fcd 100644 --- a/libraries/entities/src/EntityItemID.h +++ b/libraries/entities/src/EntityItemID.h @@ -49,11 +49,16 @@ public: EntityItemID convertToKnownIDVersion() const; EntityItemID convertToCreatorTokenVersion() const; + bool isInvalidID() const { return id == UNKNOWN_ENTITY_ID && creatorTokenID == UNKNOWN_ENTITY_TOKEN && isKnownID == false; } + // these methods allow you to create models, and later edit them. + static EntityItemID createInvalidEntityID() { return EntityItemID(UNKNOWN_ENTITY_ID, UNKNOWN_ENTITY_TOKEN, false); } static EntityItemID getIDfromCreatorTokenID(uint32_t creatorTokenID); static uint32_t getNextCreatorTokenID(); static void handleAddEntityResponse(const QByteArray& packet); static EntityItemID readEntityItemIDFromBuffer(const unsigned char* data, int bytesLeftToRead); + + QScriptValue toScriptValue(QScriptEngine* engine) const; private: friend class EntityTree; @@ -68,12 +73,22 @@ inline bool operator<(const EntityItemID& a, const EntityItemID& b) { } inline bool operator==(const EntityItemID& a, const EntityItemID& b) { + if (a.isInvalidID() && b.isInvalidID()) { + return true; + } + if (a.isInvalidID() != b.isInvalidID()) { + return false; + } if (a.id == UNKNOWN_ENTITY_ID || b.id == UNKNOWN_ENTITY_ID) { return a.creatorTokenID == b.creatorTokenID; } return a.id == b.id; } +inline bool operator!=(const EntityItemID& a, const EntityItemID& b) { + return !(a == b); +} + inline uint qHash(const EntityItemID& a, uint seed) { QUuid temp; if (a.id == UNKNOWN_ENTITY_ID) { @@ -94,5 +109,4 @@ Q_DECLARE_METATYPE(QVector); QScriptValue EntityItemIDtoScriptValue(QScriptEngine* engine, const EntityItemID& properties); void EntityItemIDfromScriptValue(const QScriptValue &object, EntityItemID& properties); - #endif // hifi_EntityItemID_h diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 8113168655..1658764c71 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -217,7 +217,8 @@ RayToEntityIntersectionResult::RayToEntityIntersectionResult() : entityID(), properties(), distance(0), - face() + face(), + entity(NULL) { } diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 1497b2966e..2150fa51da 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -25,6 +25,7 @@ class EntityTree; +class MouseEvent; class RayToEntityIntersectionResult { @@ -37,6 +38,7 @@ public: float distance; BoxFace face; glm::vec3 intersection; + EntityItem* entity; }; Q_DECLARE_METATYPE(RayToEntityIntersectionResult) @@ -101,6 +103,21 @@ signals: void entityCollisionWithVoxel(const EntityItemID& entityID, const VoxelDetail& voxel, const CollisionInfo& collision); void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const CollisionInfo& collision); + void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void mouseReleaseOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + + void clickDownOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void holdingClickOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void clickReleaseOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); + + void hoverEnterEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void hoverOverEntity(const EntityItemID& entityItemID, const MouseEvent& event); + void hoverLeaveEntity(const EntityItemID& entityItemID, const MouseEvent& event); + + void enterEntity(const EntityItemID& entityItemID); + void leaveEntity(const EntityItemID& entityItemID); + private: void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/SphereEntityItem.h b/libraries/entities/src/SphereEntityItem.h index d57ada760b..21cb58223b 100644 --- a/libraries/entities/src/SphereEntityItem.h +++ b/libraries/entities/src/SphereEntityItem.h @@ -53,6 +53,9 @@ public: virtual const Shape& getCollisionShapeInMeters() const { return _sphereShape; } + // TODO: implement proper contains for 3D ellipsoid + //virtual bool contains(const glm::vec3& point) const; + protected: virtual void recalculateCollisionShape(); diff --git a/libraries/networking/src/DataServerAccountInfo.cpp b/libraries/networking/src/DataServerAccountInfo.cpp index 600eb57166..9b513c96f1 100644 --- a/libraries/networking/src/DataServerAccountInfo.cpp +++ b/libraries/networking/src/DataServerAccountInfo.cpp @@ -130,11 +130,11 @@ const QByteArray& DataServerAccountInfo::getUsernameSignature() { reinterpret_cast(&privateKeyData), _privateKey.size()); if (rsaPrivateKey) { - QByteArray usernameByteArray = _username.toUtf8(); + QByteArray lowercaseUsername = _username.toLower().toUtf8(); _usernameSignature.resize(RSA_size(rsaPrivateKey)); - int encryptReturn = RSA_private_encrypt(usernameByteArray.size(), - reinterpret_cast(usernameByteArray.constData()), + int encryptReturn = RSA_private_encrypt(lowercaseUsername.size(), + reinterpret_cast(lowercaseUsername.constData()), reinterpret_cast(_usernameSignature.data()), rsaPrivateKey, RSA_PKCS1_PADDING); diff --git a/libraries/networking/src/HifiSockAddr.cpp b/libraries/networking/src/HifiSockAddr.cpp index 97e9721356..11674a948e 100644 --- a/libraries/networking/src/HifiSockAddr.cpp +++ b/libraries/networking/src/HifiSockAddr.cpp @@ -9,9 +9,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include -#include -#include +#include +#include +#include #include "HifiSockAddr.h" @@ -36,13 +36,20 @@ HifiSockAddr::HifiSockAddr(const HifiSockAddr& otherSockAddr) { _port = otherSockAddr._port; } -HifiSockAddr::HifiSockAddr(const QString& hostname, quint16 hostOrderPort) { - // lookup the IP by the hostname - QHostInfo hostInfo = QHostInfo::fromName(hostname); - foreach(const QHostAddress& address, hostInfo.addresses()) { - if (address.protocol() == QAbstractSocket::IPv4Protocol) { - _address = address; - _port = hostOrderPort; +HifiSockAddr::HifiSockAddr(const QString& hostname, quint16 hostOrderPort, bool shouldBlockForLookup) : + _address(hostname), + _port(hostOrderPort) +{ + // if we parsed an IPv4 address out of the hostname, don't look it up + if (_address.protocol() != QAbstractSocket::IPv4Protocol) { + // lookup the IP by the hostname + if (shouldBlockForLookup) { + qDebug() << "Asynchronously looking up IP address for hostname" << hostname; + QHostInfo result = QHostInfo::fromName(hostname); + handleLookupResult(result); + } else { + int lookupID = QHostInfo::lookupHost(hostname, this, SLOT(handleLookupResult(QHostInfo))); + qDebug() << "Synchronously looking up IP address for hostname" << hostname << "- lookup ID is" << lookupID; } } } @@ -75,6 +82,22 @@ bool HifiSockAddr::operator==(const HifiSockAddr& rhsSockAddr) const { return _address == rhsSockAddr._address && _port == rhsSockAddr._port; } +void HifiSockAddr::handleLookupResult(const QHostInfo& hostInfo) { + if (hostInfo.error() != QHostInfo::NoError) { + qDebug() << "Lookup failed for" << hostInfo.lookupId() << ":" << hostInfo.errorString(); + } + + foreach(const QHostAddress& address, hostInfo.addresses()) { + // just take the first IPv4 address + if (address.protocol() == QAbstractSocket::IPv4Protocol) { + _address = address; + qDebug() << "QHostInfo lookup result for" + << hostInfo.hostName() << "with lookup ID" << hostInfo.lookupId() << "is" << address.toString(); + break; + } + } +} + QDebug operator<<(QDebug debug, const HifiSockAddr& sockAddr) { debug.nospace() << sockAddr._address.toString().toLocal8Bit().constData() << ":" << sockAddr._port; return debug.space(); diff --git a/libraries/networking/src/HifiSockAddr.h b/libraries/networking/src/HifiSockAddr.h index 42f815390a..064f8032ca 100644 --- a/libraries/networking/src/HifiSockAddr.h +++ b/libraries/networking/src/HifiSockAddr.h @@ -19,14 +19,15 @@ #include #endif -#include +#include -class HifiSockAddr { +class HifiSockAddr : public QObject { + Q_OBJECT public: HifiSockAddr(); HifiSockAddr(const QHostAddress& address, quint16 port); HifiSockAddr(const HifiSockAddr& otherSockAddr); - HifiSockAddr(const QString& hostname, quint16 hostOrderPort); + HifiSockAddr(const QString& hostname, quint16 hostOrderPort, bool shouldBlockForLookup = false); HifiSockAddr(const sockaddr* sockaddr); bool isNull() const { return _address.isNull() && _port == 0; } @@ -51,6 +52,8 @@ public: friend QDebug operator<<(QDebug debug, const HifiSockAddr& sockAddr); friend QDataStream& operator<<(QDataStream& dataStream, const HifiSockAddr& sockAddr); friend QDataStream& operator>>(QDataStream& dataStream, HifiSockAddr& sockAddr); +private slots: + void handleLookupResult(const QHostInfo& hostInfo); private: QHostAddress _address; quint16 _port; diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 5c4dc6cea2..043f0621bb 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -73,6 +73,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short _dtlsSocket(NULL), _localSockAddr(), _publicSockAddr(), + _stunSockAddr(STUN_SERVER_HOSTNAME, STUN_SERVER_PORT), _numCollectedPackets(0), _numCollectedBytes(0), _packetStatTimer() @@ -583,11 +584,8 @@ void LimitedNodeList::sendSTUNRequest() { QUuid randomUUID = QUuid::createUuid(); memcpy(stunRequestPacket + packetIndex, randomUUID.toRfc4122().data(), NUM_TRANSACTION_ID_BYTES); - // lookup the IP for the STUN server - HifiSockAddr stunSockAddr(STUN_SERVER_HOSTNAME, STUN_SERVER_PORT); - _nodeSocket.writeDatagram((char*) stunRequestPacket, sizeof(stunRequestPacket), - stunSockAddr.getAddress(), stunSockAddr.getPort()); + _stunSockAddr.getAddress(), _stunSockAddr.getPort()); } void LimitedNodeList::rebindNodeSocket() { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 73381d01a5..64495fbd34 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -163,6 +163,7 @@ protected: QUdpSocket* _dtlsSocket; HifiSockAddr _localSockAddr; HifiSockAddr _publicSockAddr; + HifiSockAddr _stunSockAddr; int _numCollectedPackets; int _numCollectedBytes; QElapsedTimer _packetStatTimer; diff --git a/libraries/script-engine/src/MouseEvent.h b/libraries/script-engine/src/MouseEvent.h index 7555f2ea5a..1936e6b58e 100644 --- a/libraries/script-engine/src/MouseEvent.h +++ b/libraries/script-engine/src/MouseEvent.h @@ -21,6 +21,8 @@ public: static QScriptValue toScriptValue(QScriptEngine* engine, const MouseEvent& event); static void fromScriptValue(const QScriptValue& object, MouseEvent& event); + + QScriptValue toScriptValue(QScriptEngine* engine) const { return MouseEvent::toScriptValue(engine, *this); } int x; int y;