diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index af5f2c904e..db97da751f 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -935,39 +935,7 @@ void OctreeServer::handleOctreeFileReplacement(QSharedPointer m // so here we just store a special file at our persist path // and then force a stop of the server so that it can pick it up when it relaunches if (!_persistAbsoluteFilePath.isEmpty()) { - - // before we restart the server and make it try and load this data, let's make sure it is valid - auto compressedOctree = message->getMessage(); - QByteArray jsonOctree; - - // assume we have GZipped content - bool wasCompressed = gunzip(compressedOctree, jsonOctree); - if (!wasCompressed) { - // the source was not compressed, assume we were sent regular JSON data - jsonOctree = compressedOctree; - } - - // check the JSON data to verify it is an object - if (QJsonDocument::fromJson(jsonOctree).isObject()) { - if (!wasCompressed) { - // source was not compressed, we compress it before we write it locally - gzip(jsonOctree, compressedOctree); - } - - // write the compressed octree data to a special file - auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION); - QFile replacementFile(replacementFilePath); - if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { - // we've now written our replacement file, time to take the server down so it can - // process it when it comes back up - qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; - setFinished(true); - } else { - qWarning() << "Could not write replacement octree data to file - refusing to process"; - } - } else { - qDebug() << "Received replacement octree file that is invalid - refusing to process"; - } + replaceContentFromMessageData(message->getMessage()); } else { qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; } @@ -977,6 +945,68 @@ void OctreeServer::handleOctreeFileReplacement(QSharedPointer m } } +// Message->getMessage() contains a QByteArray representation of the URL to download from +void OctreeServer::handleOctreeFileReplacementFromURL(QSharedPointer message) { + qInfo() << "Received request to replace content from a url"; + if (!_isFinished && !_isShuttingDown) { + // This call comes from Interface, so we skip our domain server check + // but confirm that we have permissions to replace content sets + if (DependencyManager::get()->getThisNodeCanReplaceContent()) { + if (!_persistAbsoluteFilePath.isEmpty()) { + // Convert message data into our URL + QString url(message->getMessage()); + QUrl modelsURL = QUrl(url, QUrl::StrictMode); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest request(modelsURL); + QNetworkReply* reply = networkAccessManager.get(request); + connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() { + QNetworkReply::NetworkError networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + QByteArray contents = reply->readAll(); + replaceContentFromMessageData(contents); + } else { + qDebug() << "Error downloading JSON from specified file"; + } + }); + } else { + qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; + } + } + } +} + +void OctreeServer::replaceContentFromMessageData(QByteArray content) { + //Assume we have compressed data + auto compressedOctree = content; + QByteArray jsonOctree; + + bool wasCompressed = gunzip(compressedOctree, jsonOctree); + if (!wasCompressed) { + // the source was not compressed, assume we were sent regular JSON data + jsonOctree = compressedOctree; + } + // check the JSON data to verify it is an object + if (QJsonDocument::fromJson(jsonOctree).isObject()) { + if (!wasCompressed) { + // source was not compressed, we compress it before we write it locally + gzip(jsonOctree, compressedOctree); + } + // write the compressed octree data to a special file + auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION); + QFile replacementFile(replacementFilePath); + if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { + // we've now written our replacement file, time to take the server down so it can + // process it when it comes back up + qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; + setFinished(true); + } else { + qWarning() << "Could not write replacement octree data to file - refusing to process"; + } + } else { + qDebug() << "Received replacement octree file that is invalid - refusing to process"; + } +} + bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) { result = false; // assume it doesn't exist bool optionAvailable = false; @@ -1202,6 +1232,7 @@ void OctreeServer::domainSettingsRequestComplete() { packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket"); packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); + packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURL"); readConfiguration(); diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 8db8d845de..5043ea681c 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -137,6 +137,7 @@ private slots: void handleOctreeDataNackPacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionRequestPacket(QSharedPointer message, SharedNodePointer senderNode); void handleOctreeFileReplacement(QSharedPointer message); + void handleOctreeFileReplacementFromURL(QSharedPointer message); void removeSendThread(); protected: @@ -161,6 +162,8 @@ protected: UniqueSendThread createSendThread(const SharedNodePointer& node); virtual UniqueSendThread newSendThread(const SharedNodePointer& node); + void replaceContentFromMessageData(QByteArray content); + int _argc; const char** _argv; char** _parsedArgV; diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 6f35b76f1d..fea244873c 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -111,14 +111,14 @@ macro(SET_PACKAGING_PARAMETERS) # shortcut names if (PRODUCTION_BUILD) - set(INTERFACE_SHORTCUT_NAME "Interface") + set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface") set(CONSOLE_SHORTCUT_NAME "Sandbox") else () - set(INTERFACE_SHORTCUT_NAME "Interface - ${BUILD_VERSION}") + set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION}") set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION}") endif () - set(INTERFACE_HF_SHORTCUT_NAME "High Fidelity ${INTERFACE_SHORTCUT_NAME}") + set(INTERFACE_HF_SHORTCUT_NAME "${INTERFACE_SHORTCUT_NAME}") set(CONSOLE_HF_SHORTCUT_NAME "High Fidelity ${CONSOLE_SHORTCUT_NAME}") set(PRE_SANDBOX_INTERFACE_SHORTCUT_NAME "High Fidelity") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 85b841aa72..de79b49a74 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -811,7 +811,6 @@ Section "-Core installation" Delete "$INSTDIR\server-console.exe" RMDir /r "$INSTDIR\locales" RMDir /r "$INSTDIR\resources\app" - RMDir /r "$INSTDIR\plugins" Delete "$INSTDIR\resources\atom.asar" Delete "$INSTDIR\build-info.json" Delete "$INSTDIR\content_resources_200_percent.pak" diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c1eff76d78..8d0e949ff3 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.7, + "version": 1.8, "settings": [ { "name": "metaverse", @@ -112,7 +112,6 @@ "label": "Adult (18+)" } ] - }, { "name": "hosts", @@ -161,15 +160,15 @@ "label": "HTTP Password", "type": "password", "help": "Password used for basic HTTP authentication. Leave this alone if you do not want to change it.", - "password_placeholder" : "******", + "password_placeholder": "******", "value-hidden": true }, { - "name": "verify_http_password", - "label": "Verify HTTP Password", - "type": "password", - "help": "Must match the password entered above for change to be saved.", - "value-hidden": true + "name": "verify_http_password", + "label": "Verify HTTP Password", + "type": "password", + "help": "Must match the password entered above for change to be saved.", + "value-hidden": true }, { "name": "maximum_user_capacity", @@ -208,21 +207,19 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which types of users can have which domain-wide permissions.", + "help": "Indicate which types of users can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, - "groups": [ { "label": "Type of User", "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -276,9 +273,15 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ], - "non-deletable-row-key": "permissions_id", "non-deletable-row-values": ["localhost", "anonymous", "logged-in"] }, @@ -291,18 +294,16 @@ "can_add_new_rows": false, "new_category_placeholder": "Add Group", "new_category_message": "Save and reload to see ranks", - "groups": [ { "label": "Rank", "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -381,6 +382,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -393,18 +401,16 @@ "can_add_new_rows": false, "new_category_placeholder": "Add Blacklist Group", "new_category_message": "Save and reload to see ranks", - "groups": [ { "label": "Rank", "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -480,6 +486,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -488,18 +501,16 @@ "type": "table", "caption": "Permissions for Specific Users", "can_add_new_rows": true, - "groups": [ { "label": "User", "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -553,6 +564,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -567,11 +585,10 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -625,6 +642,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -639,11 +663,10 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -697,6 +720,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -711,11 +741,10 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 7 + "label": "Permissions ?", + "span": 8 } ], - "columns": [ { "name": "permissions_id", @@ -769,6 +798,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_replace_content", + "label": "Replace Content", + "type": "checkbox", + "editable": true, + "default": false } ] } @@ -784,7 +820,6 @@ "label": "Persistent Scripts", "help": "Add the URLs for scripts that you would like to ensure are always running in your domain.", "can_add_new_rows": true, - "columns": [ { "name": "url", @@ -946,7 +981,6 @@ "help": "In this table you can define a set of zones in which you can specify various audio properties.", "numbered": false, "can_add_new_rows": true, - "key": { "name": "name", "label": "Name", @@ -999,7 +1033,6 @@ "numbered": true, "can_order": true, "can_add_new_rows": true, - "columns": [ { "name": "source", @@ -1028,7 +1061,6 @@ "help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.", "numbered": true, "can_add_new_rows": true, - "columns": [ { "name": "zone", @@ -1063,7 +1095,9 @@ { "name": "audio_buffer", "label": "Audio Buffers", - "assignment-types": [0], + "assignment-types": [ + 0 + ], "settings": [ { "name": "dynamic_jitter_buffer", @@ -1082,35 +1116,37 @@ "advanced": true }, { - "name": "max_frames_over_desired", - "deprecated": true + "name": "max_frames_over_desired", + "deprecated": true }, { - "name": "window_starve_threshold", - "deprecated": true + "name": "window_starve_threshold", + "deprecated": true }, { - "name": "window_seconds_for_desired_calc_on_too_many_starves", - "deprecated": true + "name": "window_seconds_for_desired_calc_on_too_many_starves", + "deprecated": true }, { - "name": "window_seconds_for_desired_reduction", - "deprecated": true + "name": "window_seconds_for_desired_reduction", + "deprecated": true }, { - "name": "use_stdev_for_desired_calc", - "deprecated": true + "name": "use_stdev_for_desired_calc", + "deprecated": true }, { - "name": "repetition_with_fade", - "deprecated": true + "name": "repetition_with_fade", + "deprecated": true } ] }, { "name": "entity_server_settings", "label": "Entity Server Settings", - "assignment-types": [6], + "assignment-types": [ + 6 + ], "settings": [ { "name": "maxTmpLifetime", @@ -1167,13 +1203,32 @@ "help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.", "numbered": false, "can_add_new_rows": true, - - "default": [ - {"Name":"Half Hourly Rolling","backupInterval":1800,"format":".backup.halfhourly.%N","maxBackupVersions":5}, - {"Name":"Daily Rolling","backupInterval":86400,"format":".backup.daily.%N","maxBackupVersions":7}, - {"Name":"Weekly Rolling","backupInterval":604800,"format":".backup.weekly.%N","maxBackupVersions":4}, - {"Name":"Thirty Day Rolling","backupInterval":2592000,"format":".backup.thirtyday.%N","maxBackupVersions":12} - ], + "default": [ + { + "Name": "Half Hourly Rolling", + "backupInterval": 1800, + "format": ".backup.halfhourly.%N", + "maxBackupVersions": 5 + }, + { + "Name": "Daily Rolling", + "backupInterval": 86400, + "format": ".backup.daily.%N", + "maxBackupVersions": 7 + }, + { + "Name": "Weekly Rolling", + "backupInterval": 604800, + "format": ".backup.weekly.%N", + "maxBackupVersions": 4 + }, + { + "Name": "Thirty Day Rolling", + "backupInterval": 2592000, + "format": ".backup.thirtyday.%N", + "maxBackupVersions": 12 + } + ], "columns": [ { "name": "Name", @@ -1309,7 +1364,9 @@ { "name": "avatar_mixer", "label": "Avatar Mixer", - "assignment-types": [1], + "assignment-types": [ + 1 + ], "settings": [ { "name": "max_node_send_bandwidth", @@ -1362,7 +1419,10 @@ { "name": "downstream_servers", "label": "Receiving Servers", - "assignment-types": [0,1], + "assignment-types": [ + 0, + 1 + ], "type": "table", "advanced": true, "can_add_new_rows": true, @@ -1402,7 +1462,10 @@ { "name": "upstream_servers", "label": "Broadcasting Servers", - "assignment-types": [0,1], + "assignment-types": [ + 0, + 1 + ], "type": "table", "advanced": true, "can_add_new_rows": true, @@ -1442,4 +1505,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index fc595be67e..6951a90261 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -269,6 +269,7 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; + userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; } else { // this node is an agent const QHostAddress& addr = node->getLocalSocket().getAddress(); @@ -357,6 +358,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; + userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; newNode->setPermissions(userPerms); return newNode; } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 7a2cfa645a..d93126f2c7 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -112,6 +112,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; + const QString EDITORS_CAN_REPLACE_CONTENT_KEYPATH = "security.editors_can_replace_content"; qDebug() << "Previous domain-server settings version was" << QString::number(oldVersion, 'g', 8) << "and the new version is" @@ -294,6 +295,13 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // persist the new config so the user config file has the correctly merged config persistToFile(); } + + if (oldVersion < 1.8) { + unpackPermissions(); + // This was prior to addition of domain content replacement, add that to localhost permissions by default + _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canReplaceDomainContent); + packPermissions(); + } } unpackPermissions(); diff --git a/interface/resources/qml/controls-uit/Tree.qml b/interface/resources/qml/controls-uit/Tree.qml index 8bce092947..53f66fa67c 100644 --- a/interface/resources/qml/controls-uit/Tree.qml +++ b/interface/resources/qml/controls-uit/Tree.qml @@ -27,6 +27,7 @@ TreeView { model: treeModel selection: ItemSelectionModel { + id: selectionModel model: treeModel } @@ -215,6 +216,10 @@ TreeView { onDoubleClicked: isExpanded(index) ? collapse(index) : expand(index) + onClicked: { + selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect); + } + onActivated: { var path = scriptsModel.data(index, 0x100) if (path) { diff --git a/interface/resources/qml/controls-uit/WebGlyphButton.qml b/interface/resources/qml/controls-uit/WebGlyphButton.qml new file mode 100644 index 0000000000..15524e4188 --- /dev/null +++ b/interface/resources/qml/controls-uit/WebGlyphButton.qml @@ -0,0 +1,48 @@ +// +// GlyphButton.qml +// +// Created by Vlad Stelmahovsky on 2017-06-21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" + +Original.Button { + id: control + + property int colorScheme: hifi.colorSchemes.light + property string glyph: "" + property int size: 32 + //colors + readonly property color normalColor: "#AFAFAF" + readonly property color hoverColor: "#00B4EF" + readonly property color clickedColor: "#FFFFFF" + readonly property color disabledColor: "#575757" + + style: ButtonStyle { + background: Item {} + + + label: HiFiGlyphs { + color: control.enabled ? (control.pressed ? control.clickedColor : + (control.hovered ? control.hoverColor : control.normalColor)) : + control.disabledColor + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors { + // Tweak horizontal alignment so that it looks right. + left: parent.left + leftMargin: -0.5 + } + text: control.glyph + size: control.size + } + } +} diff --git a/interface/resources/qml/hifi/SkyboxChanger.qml b/interface/resources/qml/hifi/SkyboxChanger.qml new file mode 100644 index 0000000000..a4798ba959 --- /dev/null +++ b/interface/resources/qml/hifi/SkyboxChanger.qml @@ -0,0 +1,173 @@ +// +// skyboxchanger.qml +// +// +// Created by Cain Kilgore on 9th August 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick.Layouts 1.3 + +Rectangle { + id: root; + + color: hifi.colors.baseGray; + + Item { + id: titleBarContainer; + // Size + width: parent.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + RalewaySemiBold { + id: titleBarText; + text: "Skybox Changer"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + RalewaySemiBold { + id: titleBarDesc; + text: "Click an image to choose a new Skybox."; + wrapMode: Text.Wrap + // Text size + size: 14; + // Anchors + anchors.fill: parent; + anchors.top: titleBarText.bottom + anchors.leftMargin: 16; + anchors.rightMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + } + + // This RowLayout could be a GridLayout instead for further expandability. + // As this SkyboxChanger task only required 6 images, implementing GridLayout wasn't necessary. + // In the future if this is to be expanded to add more Skyboxes, it might be worth changing this. + RowLayout { + id: row1 + anchors.top: titleBarContainer.bottom + anchors.left: parent.left + anchors.leftMargin: 30 + Layout.fillWidth: true + anchors.topMargin: 30 + spacing: 10 + Image { + width: 200; height: 200 + fillMode: Image.Stretch + source: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/thumbnails/thumb_1.jpg" + clip: true + id: preview1 + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript({method: 'changeSkybox', url: 'http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxes/1.jpg'}); + } + } + Layout.fillWidth: true + } + Image { + width: 200; height: 200 + fillMode: Image.Stretch + source: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/thumbnails/thumb_2.jpg" + clip: true + id: preview2 + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript({method: 'changeSkybox', url: 'http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxes/2.png'}); + } + } + } + } + RowLayout { + id: row2 + anchors.top: row1.bottom + anchors.topMargin: 10 + anchors.left: parent.left + Layout.fillWidth: true + anchors.leftMargin: 30 + spacing: 10 + Image { + width: 200; height: 200 + fillMode: Image.Stretch + source: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/thumbnails/thumb_3.jpg" + clip: true + id: preview3 + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript({method: 'changeSkybox', url: 'http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxes/3.jpg'}); + } + } + } + Image { + width: 200; height: 200 + fillMode: Image.Stretch + source: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/thumbnails/thumb_4.jpg" + clip: true + id: preview4 + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript({method: 'changeSkybox', url: 'http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxes/4.jpg'}); + } + } + } + } + RowLayout { + id: row3 + anchors.top: row2.bottom + anchors.topMargin: 10 + anchors.left: parent.left + Layout.fillWidth: true + anchors.leftMargin: 30 + spacing: 10 + Image { + width: 200; height: 200 + fillMode: Image.Stretch + source: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/thumbnails/thumb_5.jpg" + clip: true + id: preview5 + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript({method: 'changeSkybox', url: 'http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxes/5.png'}); + } + } + } + Image { + width: 200; height: 200 + fillMode: Image.Stretch + source: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/thumbnails/thumb_6.jpg" + clip: true + id: preview6 + MouseArea { + anchors.fill: parent + onClicked: { + sendToScript({method: 'changeSkybox', url: 'http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxes/6.jpg'}); + } + } + } + } + + signal sendToScript(var message); + +} diff --git a/interface/resources/qml/hifi/WebBrowser.qml b/interface/resources/qml/hifi/WebBrowser.qml new file mode 100644 index 0000000000..f639586668 --- /dev/null +++ b/interface/resources/qml/hifi/WebBrowser.qml @@ -0,0 +1,253 @@ +// +// WebBrowser.qml +// +// +// Created by Vlad Stelmahovsky on 06/22/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.5 as QQControls +import QtQuick.Layouts 1.3 +import QtQuick.Controls.Styles 1.4 + +import QtWebEngine 1.2 +import QtWebChannel 1.0 + +import "../styles-uit" +import "../controls-uit" as HifiControls +import "../windows" +import "../controls" + +Rectangle { + id: root; + + HifiConstants { id: hifi; } + + property string title: ""; + signal sendToScript(var message); + property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false + property bool keyboardRaised: false + property bool punctuationMode: false + + + color: hifi.colors.baseGray; + + // only show the title if loaded through a "loader" + + Column { + spacing: 2 + width: parent.width; + + RowLayout { + width: parent.width; + height: 48 + + HifiControls.WebGlyphButton { + enabled: webEngineView.canGoBack + glyph: hifi.glyphs.backward; + anchors.verticalCenter: parent.verticalCenter; + size: 38; + onClicked: { + webEngineView.goBack() + } + } + + HifiControls.WebGlyphButton { + enabled: webEngineView.canGoForward + glyph: hifi.glyphs.forward; + anchors.verticalCenter: parent.verticalCenter; + size: 38; + onClicked: { + webEngineView.goForward() + } + } + + QQControls.TextField { + id: addressBar + + Image { + anchors.verticalCenter: addressBar.verticalCenter; + x: 5 + z: 2 + id: faviconImage + width: 16; height: 16 + sourceSize: Qt.size(width, height) + source: webEngineView.icon + } + + HifiControls.WebGlyphButton { + glyph: webEngineView.loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall; + anchors.verticalCenter: parent.verticalCenter; + width: hifi.dimensions.controlLineHeight + z: 2 + x: addressBar.width - 28 + onClicked: { + if (webEngineView.loading) { + webEngineView.stop() + } else { + reloadTimer.start() + } + } + } + + style: TextFieldStyle { + padding { + left: 26; + right: 26 + } + } + focus: true + Layout.fillWidth: true + text: webEngineView.url + onAccepted: webEngineView.url = text + } + HifiControls.WebGlyphButton { + checkable: true + //only QtWebEngine 1.3 + //checked: webEngineView.audioMuted + glyph: checked ? hifi.glyphs.unmuted : hifi.glyphs.muted + anchors.verticalCenter: parent.verticalCenter; + width: hifi.dimensions.controlLineHeight + onClicked: { + webEngineView.triggerWebAction(WebEngineView.ToggleMediaMute) + } + } + } + + QQControls.ProgressBar { + id: loadProgressBar + style: ProgressBarStyle { + background: Rectangle { + color: "#6A6A6A" + } + progress: Rectangle{ + color: "#00B4EF" + } + } + + width: parent.width; + minimumValue: 0 + maximumValue: 100 + value: webEngineView.loadProgress + height: 2 + } + + HifiControls.BaseWebView { + id: webEngineView + focus: true + objectName: "tabletWebEngineView" + + url: "http://www.highfidelity.com" + property real webViewHeight: root.height - loadProgressBar.height - 48 - 4 + + width: parent.width; + height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight + + profile: HFTabletWebEngineProfile; + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: webEngineView.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + settings.autoLoadImages: true + settings.javascriptEnabled: true + settings.errorPageEnabled: true + settings.pluginsEnabled: true + settings.fullScreenSupportEnabled: false + //from WebEngine 1.3 + // settings.autoLoadIconsForPage: false + // settings.touchIconsEnabled: false + + onCertificateError: { + error.defer(); + } + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + webEngineView.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)"; + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onNewViewRequested: { + if (!request.userInitiated) { + print("Warning: Blocked a popup window."); + } + } + + onRenderProcessTerminated: { + var status = ""; + switch (terminationStatus) { + case WebEngineView.NormalTerminationStatus: + status = "(normal exit)"; + break; + case WebEngineView.AbnormalTerminationStatus: + status = "(abnormal exit)"; + break; + case WebEngineView.CrashedTerminationStatus: + status = "(crashed)"; + break; + case WebEngineView.KilledTerminationStatus: + status = "(killed)"; + break; + } + + print("Render process exited with code " + exitCode + " " + status); + reloadTimer.running = true; + } + + onWindowCloseRequested: { + } + + Timer { + id: reloadTimer + interval: 0 + running: false + repeat: false + onTriggered: webEngineView.reload() + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } +} diff --git a/interface/resources/qml/hifi/commerce/Checkout.qml b/interface/resources/qml/hifi/commerce/Checkout.qml index 48d5f5b1d5..865bb72921 100644 --- a/interface/resources/qml/hifi/commerce/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/Checkout.qml @@ -24,12 +24,48 @@ Rectangle { HifiConstants { id: hifi; } id: checkoutRoot; - property string itemId; - property string itemHref; + property bool inventoryReceived: false; + property bool balanceReceived: false; + property string itemId: ""; + property string itemHref: ""; + property int balanceAfterPurchase: 0; + property bool alreadyOwned: false; // Style color: hifi.colors.baseGray; Hifi.QmlCommerce { id: commerce; + onBuyResult: { + if (failureMessage.length) { + buyButton.text = "Buy Failed"; + buyButton.enabled = false; + } else { + if (urlHandler.canHandleUrl(itemHref)) { + urlHandler.handleUrl(itemHref); + } + sendToScript({method: 'checkout_buySuccess', itemId: itemId}); + } + } + onBalanceResult: { + if (failureMessage.length) { + console.log("Failed to get balance", failureMessage); + } else { + balanceReceived = true; + hfcBalanceText.text = balance; + balanceAfterPurchase = balance - parseInt(itemPriceText.text, 10); + } + } + onInventoryResult: { + if (failureMessage.length) { + console.log("Failed to get inventory", failureMessage); + } else { + inventoryReceived = true; + if (inventoryContains(inventory.assets, itemId)) { + alreadyOwned = true; + } else { + alreadyOwned = false; + } + } + } } // @@ -172,11 +208,56 @@ Rectangle { } } + // HFC Balance text + Item { + id: hfcBalanceContainer; + // Anchors + anchors.top: itemAuthorContainer.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: childrenRect.height; + + RalewaySemiBold { + id: hfcBalanceTextLabel; + text: "HFC Balance:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Text size + size: 20; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + RalewayRegular { + id: hfcBalanceText; + text: "--"; + // Text size + size: hfcBalanceTextLabel.size; + // Anchors + anchors.top: parent.top; + anchors.left: hfcBalanceTextLabel.right; + anchors.leftMargin: 16; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + // Item Price text Item { id: itemPriceContainer; // Anchors - anchors.top: itemAuthorContainer.bottom; + anchors.top: hfcBalanceContainer.bottom; anchors.topMargin: 4; anchors.left: parent.left; anchors.leftMargin: 16; @@ -215,6 +296,51 @@ Rectangle { verticalAlignment: Text.AlignVCenter; } } + + // HFC "Balance After Purchase" text + Item { + id: hfcBalanceAfterPurchaseContainer; + // Anchors + anchors.top: itemPriceContainer.bottom; + anchors.topMargin: 4; + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + height: childrenRect.height; + + RalewaySemiBold { + id: hfcBalanceAfterPurchaseTextLabel; + text: "HFC Balance After Purchase:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Text size + size: 20; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + RalewayRegular { + id: hfcBalanceAfterPurchaseText; + text: balanceAfterPurchase; + // Text size + size: hfcBalanceAfterPurchaseTextLabel.size; + // Anchors + anchors.top: parent.top; + anchors.left: hfcBalanceAfterPurchaseTextLabel.right; + anchors.leftMargin: 16; + width: paintedWidth; + // Style + color: (balanceAfterPurchase >= 0) ? hifi.colors.lightGrayText : hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } } // // ITEM DESCRIPTION END @@ -231,7 +357,8 @@ Rectangle { height: 40; // Anchors anchors.left: parent.left; - anchors.top: itemDescriptionContainer.bottom; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; // "Cancel" button HifiControlsUit.Button { @@ -253,8 +380,8 @@ Rectangle { // "Buy" button HifiControlsUit.Button { - property bool buyFailed: false; id: buyButton; + enabled: balanceAfterPurchase >= 0 && !alreadyOwned && inventoryReceived && balanceReceived; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark; anchors.top: parent.top; @@ -264,13 +391,9 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 20; width: parent.width/2 - anchors.rightMargin*2; - text: "Buy" + text: (inventoryReceived && balanceReceived) ? (alreadyOwned ? "Already Owned" : "Buy") : "--"; onClicked: { - if (buyFailed) { - sendToScript({method: 'checkout_cancelClicked', params: itemId}); - } else { - sendToScript({method: 'checkout_buyClicked', success: commerce.buy(itemId, parseInt(itemPriceText.text)), itemId: itemId, itemHref: itemHref}); - } + commerce.buy(itemId, parseInt(itemPriceText.text)); } } } @@ -302,12 +425,8 @@ Rectangle { itemAuthorText.text = message.params.itemAuthor; itemPriceText.text = message.params.itemPrice; itemHref = message.params.itemHref; - buyButton.text = "Buy"; - buyButton.buyFailed = false; - break; - case 'buyFailed': - buyButton.text = "Buy Failed"; - buyButton.buyFailed = true; + commerce.balance(); + commerce.inventory(); break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); @@ -315,6 +434,15 @@ Rectangle { } signal sendToScript(var message); + function inventoryContains(inventoryJson, id) { + for (var idx = 0; idx < inventoryJson.length; idx++) { + if(inventoryJson[idx].id === id) { + return true; + } + } + return false; + } + // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/Inventory.qml b/interface/resources/qml/hifi/commerce/Inventory.qml new file mode 100644 index 0000000000..298abebdab --- /dev/null +++ b/interface/resources/qml/hifi/commerce/Inventory.qml @@ -0,0 +1,275 @@ +// +// Inventory.qml +// qml/hifi/commerce +// +// Inventory +// +// Created by Zach Fox on 2017-08-10 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +// references XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: inventoryRoot; + property string referrerURL: ""; + // Style + color: hifi.colors.baseGray; + Hifi.QmlCommerce { + id: commerce; + onBalanceResult: { + if (failureMessage.length) { + console.log("Failed to get balance", failureMessage); + } else { + hfcBalanceText.text = balance; + } + } + onInventoryResult: { + if (failureMessage.length) { + console.log("Failed to get inventory", failureMessage); + } else { + inventoryContentsList.model = inventory.assets; + } + } + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: inventoryRoot.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // Title Bar text + RalewaySemiBold { + id: titleBarText; + text: "Inventory"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // HFC BALANCE START + // + Item { + id: hfcBalanceContainer; + // Size + width: inventoryRoot.width; + height: childrenRect.height + 20; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.top: titleBarContainer.bottom; + anchors.topMargin: 4; + + RalewaySemiBold { + id: hfcBalanceTextLabel; + text: "HFC Balance:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Text size + size: 20; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + RalewayRegular { + id: hfcBalanceText; + text: "--"; + // Text size + size: hfcBalanceTextLabel.size; + // Anchors + anchors.top: parent.top; + anchors.left: hfcBalanceTextLabel.right; + anchors.leftMargin: 16; + width: paintedWidth; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + } + // + // HFC BALANCE END + // + + // + // INVENTORY CONTENTS START + // + Item { + id: inventoryContentsContainer; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + anchors.top: hfcBalanceContainer.bottom; + anchors.topMargin: 8; + anchors.bottom: actionButtonsContainer.top; + anchors.bottomMargin: 8; + + RalewaySemiBold { + id: inventoryContentsLabel; + text: "Inventory:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + // Text size + size: 24; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + ListView { + id: inventoryContentsList; + // Anchors + anchors.top: inventoryContentsLabel.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width; + delegate: Item { + width: parent.width; + height: 30; + RalewayRegular { + id: thisItemId; + // Text size + size: 20; + // Style + color: hifi.colors.blueAccent; + text: modelData.title; + // Alignment + horizontalAlignment: Text.AlignHLeft; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + sendToScript({method: 'inventory_itemClicked', itemId: modelData.id}); + } + onEntered: { + thisItemId.color = hifi.colors.blueHighlight; + } + onExited: { + thisItemId.color = hifi.colors.blueAccent; + } + } + } + } + } + // + // INVENTORY CONTENTS END + // + + // + // ACTION BUTTONS START + // + Item { + id: actionButtonsContainer; + // Size + width: inventoryRoot.width; + height: 40; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + + // "Back" button + HifiControlsUit.Button { + id: backButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.top: parent.top; + anchors.topMargin: 3; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 3; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: parent.width/2 - anchors.leftMargin*2; + text: "Back" + onClicked: { + sendToScript({method: 'inventory_backClicked', referrerURL: referrerURL}); + } + } + } + // + // ACTION BUTTONS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript, in this case the Marketplaces JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + case 'updateInventory': + referrerURL = message.referrerURL; + commerce.balance(); + commerce.inventory(); + break; + default: + console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 750c41d48a..ab1c85d928 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -229,6 +229,7 @@ static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; static const QString SNAPSHOT_EXTENSION = ".jpg"; static const QString SVO_EXTENSION = ".svo"; static const QString SVO_JSON_EXTENSION = ".svo.json"; +static const QString JSON_GZ_EXTENSION = ".json.gz"; static const QString JSON_EXTENSION = ".json"; static const QString JS_EXTENSION = ".js"; static const QString FST_EXTENSION = ".fst"; @@ -262,6 +263,8 @@ static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; +static const QString DOMAIN_SPAWNING_POINT = "/0, -10, 0"; + const QHash Application::_acceptedExtensions { { SVO_EXTENSION, &Application::importSVOFromURL }, { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, @@ -269,6 +272,7 @@ const QHash Application::_acceptedExtensi { JSON_EXTENSION, &Application::importJSONFromURL }, { JS_EXTENSION, &Application::askToLoadScript }, { FST_EXTENSION, &Application::askToSetAvatarUrl }, + { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, { ZIP_EXTENSION, &Application::importFromZIP } }; @@ -719,7 +723,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo qInstallMessageHandler(messageHandler); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); - _window->setWindowTitle("Interface"); + _window->setWindowTitle("High Fidelity Interface"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us @@ -2832,7 +2836,6 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { bool Application::importJSONFromURL(const QString& urlString) { // we only load files that terminate in just .json (not .svo.json and not .ava.json) // if they come from the High Fidelity Marketplace Assets CDN - QUrl jsonURL { urlString }; if (jsonURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { @@ -6227,6 +6230,55 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) { return true; } +bool Application::askToReplaceDomainContent(const QString& url) { + QString methodDetails; + if (DependencyManager::get()->getThisNodeCanReplaceContent()) { + QUrl originURL { url }; + if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + // Create a confirmation dialog when this call is made + const int MAX_CHARACTERS_PER_LINE = 90; + static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " + "If you want to save what you have now, create a backup before proceeding. For more information about backing up " + "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + + "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; + + bool agreeToReplaceContent = false; // assume false + agreeToReplaceContent = QMessageBox::Yes == OffscreenUi::question("Are you sure you want to replace this domain's content set?", + infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (agreeToReplaceContent) { + // Given confirmation, send request to domain server to replace content + qCDebug(interfaceapp) << "Attempting to replace domain content: " << url; + QByteArray urlData(url.toUtf8()); + auto limitedNodeList = DependencyManager::get(); + limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { + return node->getType() == NodeType::EntityServer && node->getActiveSocket(); + }, [&urlData, limitedNodeList](const SharedNodePointer& octreeNode) { + auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); + octreeFilePacket->write(urlData); + limitedNodeList->sendPacket(std::move(octreeFilePacket), *octreeNode); + }); + DependencyManager::get()->handleLookupString(DOMAIN_SPAWNING_POINT); + methodDetails = "SuccessfulRequestToReplaceContent"; + } else { + methodDetails = "UserDeclinedToReplaceContent"; + } + } else { + methodDetails = "ContentSetDidNotOriginateFromMarketplace"; + } + } else { + methodDetails = "UserDoesNotHavePermissionToReplaceContent"; + OffscreenUi::warning("Unable to replace content", "You do not have permissions to replace domain content", + QMessageBox::Ok, QMessageBox::Ok); + } + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + return true; +} + void Application::displayAvatarAttachmentWarning(const QString& message) const { auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); OffscreenUi::warning(avatarAttachmentWarningTitle, message); diff --git a/interface/src/Application.h b/interface/src/Application.h index 8a38116fe8..d0cda4105e 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -437,6 +437,8 @@ private slots: void displayAvatarAttachmentWarning(const QString& message) const; bool displayAvatarAttachmentConfirmationDialog(const QString& name) const; + bool askToReplaceDomainContent(const QString& url); + void setSessionUUID(const QUuid& sessionUUID) const; void domainChanged(const QString& domainHostname); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 56bd3eb749..2c4a515736 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -555,6 +555,8 @@ Menu::Menu() { avatar.get(), SLOT(setEnableDebugDrawIKConstraints(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderIKChains, 0, false, avatar.get(), SLOT(setEnableDebugDrawIKChains(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderDetailedCollision, 0, false, + avatar.get(), SLOT(setEnableDebugDrawDetailedCollision(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ActionMotorControl, Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar.get(), SLOT(updateMotionBehaviorFromMenu()), diff --git a/interface/src/Menu.h b/interface/src/Menu.h index a81ef9ac86..4e21cfa4ac 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -162,6 +162,7 @@ namespace MenuOption { const QString RenderIKTargets = "Show IK Targets"; const QString RenderIKConstraints = "Show IK Constraints"; const QString RenderIKChains = "Show IK Chains"; + const QString RenderDetailedCollision = "Show Detailed Collision"; const QString ResetAvatarSize = "Reset Avatar Size"; const QString ResetSensors = "Reset Sensors"; const QString RunningScripts = "Running Scripts..."; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 7f46a9e3bf..30c6a85470 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1060,6 +1060,10 @@ void MyAvatar::setEnableDebugDrawIKConstraints(bool isEnabled) { _enableDebugDrawIKConstraints = isEnabled; } +void MyAvatar::setEnableDebugDrawDetailedCollision(bool isEnabled) { + _enableDebugDrawDetailedCollision = isEnabled; +} + void MyAvatar::setEnableDebugDrawIKChains(bool isEnabled) { _enableDebugDrawIKChains = isEnabled; } @@ -1805,6 +1809,37 @@ void MyAvatar::postUpdate(float deltaTime) { AnimPose postUpdateRoomPose(_sensorToWorldMatrix); updateHoldActions(_prePhysicsRoomPose, postUpdateRoomPose); + + if (_enableDebugDrawDetailedCollision) { + AnimPose rigToWorldPose(glm::vec3(1.0f), getRotation() * Quaternions::Y_180, getPosition()); + const int NUM_DEBUG_COLORS = 8; + const glm::vec4 DEBUG_COLORS[NUM_DEBUG_COLORS] = { + glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), + glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), + glm::vec4(0.25f, 0.25f, 1.0f, 1.0f), + glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + glm::vec4(0.25f, 1.0f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.25f, 1.0f, 1.0f), + glm::vec4(1.0f, 0.65f, 0.0f, 1.0f) // Orange you glad I added this color? + }; + + if (_skeletonModel && _skeletonModel->isLoaded()) { + const Rig& rig = _skeletonModel->getRig(); + const FBXGeometry& geometry = _skeletonModel->getFBXGeometry(); + for (int i = 0; i < rig.getJointStateCount(); i++) { + AnimPose jointPose; + rig.getAbsoluteJointPoseInRigFrame(i, jointPose); + const FBXJointShapeInfo& shapeInfo = geometry.joints[i].shapeInfo; + const AnimPose pose = rigToWorldPose * jointPose; + for (size_t j = 0; j < shapeInfo.debugLines.size() / 2; j++) { + glm::vec3 pointA = pose.xformPoint(shapeInfo.debugLines[2 * j]); + glm::vec3 pointB = pose.xformPoint(shapeInfo.debugLines[2 * j + 1]); + DebugDraw::getInstance().drawRay(pointA, pointB, DEBUG_COLORS[i % NUM_DEBUG_COLORS]); + } + } + } + } } void MyAvatar::preDisplaySide(RenderArgs* renderArgs) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index dc4357be52..c7fe309894 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -553,6 +553,7 @@ public slots: void setEnableDebugDrawIKTargets(bool isEnabled); void setEnableDebugDrawIKConstraints(bool isEnabled); void setEnableDebugDrawIKChains(bool isEnabled); + void setEnableDebugDrawDetailedCollision(bool isEnabled); bool getEnableMeshVisible() const { return _skeletonModel->isVisible(); } void setEnableMeshVisible(bool isEnabled); @@ -757,6 +758,7 @@ private: bool _enableDebugDrawIKTargets { false }; bool _enableDebugDrawIKConstraints { false }; bool _enableDebugDrawIKChains { false }; + bool _enableDebugDrawDetailedCollision { false }; AudioListenerMode _audioListenerMode; glm::vec3 _customListenPosition; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 6d468c3f30..89e4368515 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -124,12 +124,26 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } - params.bodyCapsuleRadius = myAvatar->getCharacterController()->getCapsuleRadius(); - params.bodyCapsuleHalfHeight = myAvatar->getCharacterController()->getCapsuleHalfHeight(); - params.bodyCapsuleLocalOffset = myAvatar->getCharacterController()->getCapsuleLocalOffset(); - params.isTalking = head->getTimeWithoutTalking() <= 1.5f; + // pass detailed torso k-dops to rig. + int hipsJoint = _rig.indexOfJoint("Hips"); + if (hipsJoint >= 0) { + params.hipsShapeInfo = geometry.joints[hipsJoint].shapeInfo; + } + int spineJoint = _rig.indexOfJoint("Spine"); + if (spineJoint >= 0) { + params.spineShapeInfo = geometry.joints[spineJoint].shapeInfo; + } + int spine1Joint = _rig.indexOfJoint("Spine1"); + if (spine1Joint >= 0) { + params.spine1ShapeInfo = geometry.joints[spine1Joint].shapeInfo; + } + int spine2Joint = _rig.indexOfJoint("Spine2"); + if (spine2Joint >= 0) { + params.spine2ShapeInfo = geometry.joints[spine2Joint].shapeInfo; + } + _rig.updateFromControllerParameters(params, deltaTime); Rig::CharacterControllerState ccState = convertCharacterControllerState(myAvatar->getCharacterController()->getState()); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 41dd42ea22..ad79a836ad 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -16,7 +16,7 @@ #include "Ledger.h" #include "CommerceLogging.h" -bool Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername) { +void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername) { QJsonObject transaction; transaction["hfc_key"] = hfc_key; transaction["hfc"] = cost; @@ -34,32 +34,48 @@ bool Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, cons qCInfo(commerce) << "Transaction:" << QJsonDocument(request).toJson(QJsonDocument::Compact); // FIXME: talk to server instead - QStringList keySet{ hfc_key }; - if (initializedBalance() < cost) return false; + if (_inventory.contains(asset_id)) { + // This is here more for testing than as a definition of semantics. + // When we have popcerts, you will certainly be able to buy a new instance of an item that you already own a different instance of. + // I'm not sure what the server should do for now in this project's MVP. + return emit buyResult("Already owned."); + } + if (initializedBalance() < cost) { + return emit buyResult("Insufficient funds."); + } _balance -= cost; - _inventory.push_back(asset_id); - return true; // FIXME send to server. + QJsonObject inventoryAdditionObject; + inventoryAdditionObject["id"] = asset_id; + inventoryAdditionObject["title"] = "Test Title"; + inventoryAdditionObject["preview"] = "https://www.aspca.org/sites/default/files/cat-care_cat-nutrition-tips_overweight_body4_left.jpg"; + _inventory.push_back(inventoryAdditionObject); + emit buyResult(""); } bool Ledger::receiveAt(const QString& hfc_key) { auto accountManager = DependencyManager::get(); if (!accountManager->isLoggedIn()) { qCWarning(commerce) << "Cannot set receiveAt when not logged in."; - return false; + emit receiveAtResult("Not logged in"); + return false; // We know right away that we will fail, so tell the caller. } auto username = accountManager->getAccountInfo().getUsername(); qCInfo(commerce) << "Setting default receiving key for" << username; - return true; // FIXME send to server. + emit receiveAtResult(""); // FIXME: talk to server instead. + return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in. } -int Ledger::balance(const QStringList& keys) { +void Ledger::balance(const QStringList& keys) { // FIXME: talk to server instead qCInfo(commerce) << "Balance:" << initializedBalance(); - return _balance; + emit balanceResult(_balance, ""); } -QStringList Ledger::inventory(const QStringList& keys) { +void Ledger::inventory(const QStringList& keys) { // FIXME: talk to server instead - qCInfo(commerce) << "Inventory:" << _inventory; - return _inventory; + QJsonObject inventoryObject; + inventoryObject.insert("success", true); + inventoryObject.insert("assets", _inventory); + qCInfo(commerce) << "Inventory:" << inventoryObject; + emit inventoryResult(inventoryObject, ""); } \ No newline at end of file diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 092af13d75..74ed8c1ab3 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -15,21 +15,29 @@ #define hifi_Ledger_h #include +#include +#include class Ledger : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: - bool buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername = ""); + void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername = ""); bool receiveAt(const QString& hfc_key); - int balance(const QStringList& keys); - QStringList inventory(const QStringList& keys); + void balance(const QStringList& keys); + void inventory(const QStringList& keys); + +signals: + void buyResult(const QString& failureReason); + void receiveAtResult(const QString& failureReason); + void balanceResult(int balance, const QString& failureReason); + void inventoryResult(QJsonObject inventory, const QString& failureReason); private: // These in-memory caches is temporary, until we start sending things to the server. int _balance{ -1 }; - QStringList _inventory{}; + QJsonArray _inventory{}; int initializedBalance() { if (_balance < 0) _balance = 100; return _balance; } }; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 2f7d1f0b34..63bfca4f31 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -17,29 +17,35 @@ HIFI_QML_DEF(QmlCommerce) -bool QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUsername) { +QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { + auto ledger = DependencyManager::get(); + connect(ledger.data(), &Ledger::buyResult, this, &QmlCommerce::buyResult); + connect(ledger.data(), &Ledger::balanceResult, this, &QmlCommerce::balanceResult); + connect(ledger.data(), &Ledger::inventoryResult, this, &QmlCommerce::inventoryResult); +} + +void QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUsername) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList keys = wallet->listPublicKeys(); if (keys.count() == 0) { - return false; + return emit buyResult("Uninitialized Wallet."); } QString key = keys[0]; // For now, we receive at the same key that pays for it. - bool success = ledger->buy(key, cost, assetId, key, buyerUsername); + ledger->buy(key, cost, assetId, key, buyerUsername); // FIXME: until we start talking to server, report post-transaction balance and inventory so we can see log for testing. balance(); inventory(); - return success; } -int QmlCommerce::balance() { +void QmlCommerce::balance() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); - return ledger->balance(wallet->listPublicKeys()); + ledger->balance(wallet->listPublicKeys()); } -QStringList QmlCommerce::inventory() { +void QmlCommerce::inventory() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); - return ledger->inventory(wallet->listPublicKeys()); + ledger->inventory(wallet->listPublicKeys()); } \ No newline at end of file diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index fd695b2d1c..0b1d232fd7 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -21,13 +21,20 @@ class QmlCommerce : public OffscreenQmlDialog { Q_OBJECT HIFI_QML_DECL +public: + QmlCommerce(QQuickItem* parent = nullptr); + signals: void buyResult(const QString& failureMessage); + // Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and + // because we can't scalably know of out-of-band changes (e.g., another machine interacting with the block chain). + void balanceResult(int balance, const QString& failureMessage); + void inventoryResult(QJsonObject inventory, const QString& failureMessage); protected: - Q_INVOKABLE bool buy(const QString& assetId, int cost, const QString& buyerUsername = ""); - Q_INVOKABLE int balance(); - Q_INVOKABLE QStringList inventory(); + Q_INVOKABLE void buy(const QString& assetId, int cost, const QString& buyerUsername = ""); + Q_INVOKABLE void balance(); + Q_INVOKABLE void inventory(); }; #endif // hifi_QmlCommerce_h diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index d2a6ae4809..34d89b54b0 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -25,7 +25,12 @@ bool Wallet::generateKeyPair() { // FIXME: need private key, too, and persist in file. qCInfo(commerce) << "Generating keypair."; QString key = QUuid::createUuid().toString(); - _publicKeys.push_back(key); + + _publicKeys.push_back(key); // Keep in memory for synchronous speed. + // It's arguable whether we want to change the receiveAt every time, but: + // 1. It's certainly needed the first time, when createIfNeeded answers true. + // 2. It is maximally private, and we can step back from that later if desired. + // 3. It maximally exercises all the machinery, so we are most likely to surface issues now. auto ledger = DependencyManager::get(); return ledger->receiveAt(key); } diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 7cfb14c30d..79de5e81da 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -21,6 +21,7 @@ class Wallet : public QObject, public Dependency { SINGLETON_DEPENDENCY public: + // These are currently blocking calls, although they might take a moment. bool createIfNeeded(); bool generateKeyPair(); QStringList listPublicKeys(); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 706186685d..1c3df3210c 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -315,21 +315,21 @@ void setupPreferences() { static const QString RENDER("Graphics"); auto renderConfig = qApp->getRenderEngine()->getConfiguration(); if (renderConfig) { - auto ambientOcclusionConfig = renderConfig->getConfig(); - if (ambientOcclusionConfig) { - auto getter = [ambientOcclusionConfig]()->QString { return ambientOcclusionConfig->getPreset(); }; - auto setter = [ambientOcclusionConfig](QString preset) { ambientOcclusionConfig->setPreset(preset); }; + auto mainViewAmbientOcclusionConfig = renderConfig->getConfig("RenderMainView.AmbientOcclusion"); + if (mainViewAmbientOcclusionConfig) { + auto getter = [mainViewAmbientOcclusionConfig]()->QString { return mainViewAmbientOcclusionConfig->getPreset(); }; + auto setter = [mainViewAmbientOcclusionConfig](QString preset) { mainViewAmbientOcclusionConfig->setPreset(preset); }; auto preference = new ComboBoxPreference(RENDER, "Ambient occlusion", getter, setter); - preference->setItems(ambientOcclusionConfig->getPresetList()); + preference->setItems(mainViewAmbientOcclusionConfig->getPresetList()); preferences->addPreference(preference); } - auto shadowConfig = renderConfig->getConfig(); - if (shadowConfig) { - auto getter = [shadowConfig]()->QString { return shadowConfig->getPreset(); }; - auto setter = [shadowConfig](QString preset) { shadowConfig->setPreset(preset); }; + auto mainViewShadowConfig = renderConfig->getConfig("RenderMainView.RenderShadowTask"); + if (mainViewShadowConfig) { + auto getter = [mainViewShadowConfig]()->QString { return mainViewShadowConfig->getPreset(); }; + auto setter = [mainViewShadowConfig](QString preset) { mainViewShadowConfig->setPreset(preset); }; auto preference = new ComboBoxPreference(RENDER, "Shadows", getter, setter); - preference->setItems(shadowConfig->getPresetList()); + preference->setItems(mainViewShadowConfig->getPresetList()); preferences->addPreference(preference); } } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 03a75c71dd..46fb2df007 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -55,6 +55,8 @@ ContextOverlayInterface::ContextOverlayInterface() { _contextOverlayJustClicked = false; } }); + auto entityScriptingInterface = DependencyManager::get().data(); + connect(entityScriptingInterface, &EntityScriptingInterface::deletingEntity, this, &ContextOverlayInterface::deletingEntity); } static const uint32_t LEFT_HAND_HW_ID = 1; @@ -278,3 +280,9 @@ void ContextOverlayInterface::disableEntityHighlight(const EntityItemID& entityI } }); } + +void ContextOverlayInterface::deletingEntity(const EntityItemID& entityID) { + if (_currentEntityWithContextOverlay == entityID) { + destroyContextOverlay(_currentEntityWithContextOverlay, PointerEvent()); + } +} diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index ba9cb68575..b386de08cc 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -80,6 +80,7 @@ private: void enableEntityHighlight(const EntityItemID& entityItemID); void disableEntityHighlight(const EntityItemID& entityItemID); + void deletingEntity(const EntityItemID& entityItemID); }; #endif // hifi_ContextOverlayInterface_h diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 900c79fc3f..d8fe0e6bb8 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -32,6 +32,8 @@ Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : _length = line3DOverlay->getLength(); _endParentID = line3DOverlay->getEndParentID(); _endParentJointIndex = line3DOverlay->getEndJointIndex(); + _glow = line3DOverlay->getGlow(); + _glowWidth = line3DOverlay->getGlowWidth(); } Line3DOverlay::~Line3DOverlay() { @@ -138,11 +140,9 @@ void Line3DOverlay::render(RenderArgs* args) { // TODO: add support for color to renderDashedLine() geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); geometryCache->renderDashedLine(*batch, start, end, colorv4, _geometryCacheID); - } else if (_glow > 0.0f) { - geometryCache->renderGlowLine(*batch, start, end, colorv4, _glow, _glowWidth, _geometryCacheID); } else { - geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); - geometryCache->renderLine(*batch, start, end, colorv4, _geometryCacheID); + // renderGlowLine handles both glow = 0 and glow > 0 cases + geometryCache->renderGlowLine(*batch, start, end, colorv4, _glow, _glowWidth, _geometryCacheID); } } } @@ -228,9 +228,9 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { } } - auto glowWidth = properties["glow"]; + auto glowWidth = properties["glowWidth"]; if (glowWidth.isValid()) { - setGlow(glowWidth.toFloat()); + setGlowWidth(glowWidth.toFloat()); } } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 21f98d3e01..3a31ccd25f 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1099,35 +1099,139 @@ void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPos } } -void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool leftArmEnabled, bool rightArmEnabled, float dt, - const AnimPose& leftHandPose, const AnimPose& rightHandPose, - float bodyCapsuleRadius, float bodyCapsuleHalfHeight, const glm::vec3& bodyCapsuleLocalOffset) { +const float INV_SQRT_3 = 1.0f / sqrtf(3.0f); +const int DOP14_COUNT = 14; +const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = { + Vectors::UNIT_X, + -Vectors::UNIT_X, + Vectors::UNIT_Y, + -Vectors::UNIT_Y, + Vectors::UNIT_Z, + -Vectors::UNIT_Z, + glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), + -glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), + glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), + -glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), + glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), + -glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), + glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3), + -glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3) +}; - // Use this capsule to represent the avatar body. - int hipsIndex = indexOfJoint("Hips"); - glm::vec3 hipsTrans; - if (hipsIndex >= 0) { - hipsTrans = _internalPoseSet._absolutePoses[hipsIndex].trans(); +// returns true if the given point lies inside of the k-dop, specified by shapeInfo & shapePose. +// if the given point does lie within the k-dop, it also returns the amount of displacement necessary to push that point outward +// such that it lies on the surface of the kdop. +static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& shapePose, const FBXJointShapeInfo& shapeInfo, glm::vec3& displacementOut) { + + // transform point into local space of jointShape. + glm::vec3 localPoint = shapePose.inverse().xformPoint(point); + + // Only works for 14-dop shape infos. + assert(shapeInfo.dots.size() == DOP14_COUNT); + if (shapeInfo.dots.size() != DOP14_COUNT) { + return false; } - const glm::vec3 bodyCapsuleCenter = hipsTrans - bodyCapsuleLocalOffset; - const glm::vec3 bodyCapsuleStart = bodyCapsuleCenter - glm::vec3(0, bodyCapsuleHalfHeight, 0); - const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, bodyCapsuleHalfHeight, 0); + glm::vec3 minDisplacement(FLT_MAX); + float minDisplacementLen = FLT_MAX; + glm::vec3 p = localPoint - shapeInfo.avgPoint; + float pLen = glm::length(p); + if (pLen > 0.0f) { + int slabCount = 0; + for (int i = 0; i < DOP14_COUNT; i++) { + float dot = glm::dot(p, DOP14_NORMALS[i]); + if (dot > 0.0f && dot < shapeInfo.dots[i]) { + slabCount++; + float distToPlane = pLen * (shapeInfo.dots[i] / dot); + float displacementLen = distToPlane - pLen; + + // keep track of the smallest displacement + if (displacementLen < minDisplacementLen) { + minDisplacementLen = displacementLen; + minDisplacement = (p / pLen) * displacementLen; + } + } + } + if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) { + // we are within the k-dop so push the point along the minimum displacement found + displacementOut = shapePose.xformVectorFast(minDisplacement); + return true; + } else { + // point is outside of kdop + return false; + } + } else { + // point is directly on top of shapeInfo.avgPoint. + // push the point out along the x axis. + displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]); + return true; + } +} + +glm::vec3 Rig::deflectHandFromTorso(const glm::vec3& handPosition, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, + const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) const { + glm::vec3 position = handPosition; + glm::vec3 displacement; + int hipsJoint = indexOfJoint("Hips"); + if (hipsJoint >= 0) { + AnimPose hipsPose; + if (getAbsoluteJointPoseInRigFrame(hipsJoint, hipsPose)) { + if (findPointKDopDisplacement(position, hipsPose, hipsShapeInfo, displacement)) { + position += displacement; + } + } + } + + int spineJoint = indexOfJoint("Spine"); + if (spineJoint >= 0) { + AnimPose spinePose; + if (getAbsoluteJointPoseInRigFrame(spineJoint, spinePose)) { + if (findPointKDopDisplacement(position, spinePose, spineShapeInfo, displacement)) { + position += displacement; + } + } + } + + int spine1Joint = indexOfJoint("Spine1"); + if (spine1Joint >= 0) { + AnimPose spine1Pose; + if (getAbsoluteJointPoseInRigFrame(spine1Joint, spine1Pose)) { + if (findPointKDopDisplacement(position, spine1Pose, spine1ShapeInfo, displacement)) { + position += displacement; + } + } + } + + int spine2Joint = indexOfJoint("Spine2"); + if (spine2Joint >= 0) { + AnimPose spine2Pose; + if (getAbsoluteJointPoseInRigFrame(spine2Joint, spine2Pose)) { + if (findPointKDopDisplacement(position, spine2Pose, spine2ShapeInfo, displacement)) { + position += displacement; + } + } + } + + return position; +} + +void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool leftArmEnabled, bool rightArmEnabled, float dt, + const AnimPose& leftHandPose, const AnimPose& rightHandPose, + const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, + const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) { - const float HAND_RADIUS = 0.05f; const float ELBOW_POLE_VECTOR_BLEND_FACTOR = 0.95f; + int hipsIndex = indexOfJoint("Hips"); + if (leftHandEnabled) { glm::vec3 handPosition = leftHandPose.trans(); glm::quat handRotation = leftHandPose.rot(); if (!hipsEnabled) { - // prevent the hand IK targets from intersecting the body capsule - glm::vec3 displacement; - if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { - handPosition -= displacement; - } + // prevent the hand IK targets from intersecting the torso + handPosition = deflectHandFromTorso(handPosition, hipsShapeInfo, spineShapeInfo, spine1ShapeInfo, spine2ShapeInfo); } _animVars.set("leftHandPosition", handPosition); @@ -1173,11 +1277,8 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab glm::quat handRotation = rightHandPose.rot(); if (!hipsEnabled) { - // prevent the hand IK targets from intersecting the body capsule - glm::vec3 displacement; - if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { - handPosition -= displacement; - } + // prevent the hand IK targets from intersecting the torso + handPosition = deflectHandFromTorso(handPosition, hipsShapeInfo, spineShapeInfo, spine1ShapeInfo, spine2ShapeInfo); } _animVars.set("rightHandPosition", handPosition); @@ -1414,7 +1515,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo updateHands(leftHandEnabled, rightHandEnabled, hipsEnabled, leftArmEnabled, rightArmEnabled, dt, params.primaryControllerPoses[PrimaryControllerType_LeftHand], params.primaryControllerPoses[PrimaryControllerType_RightHand], - params.bodyCapsuleRadius, params.bodyCapsuleHalfHeight, params.bodyCapsuleLocalOffset); + params.hipsShapeInfo, params.spineShapeInfo, params.spine1ShapeInfo, params.spine2ShapeInfo); updateFeet(leftFootEnabled, rightFootEnabled, params.primaryControllerPoses[PrimaryControllerType_LeftFoot], params.primaryControllerPoses[PrimaryControllerType_RightFoot]); @@ -1730,7 +1831,7 @@ void Rig::computeAvatarBoundingCapsule( const FBXJointShapeInfo& shapeInfo = geometry.joints.at(index).shapeInfo; AnimPose pose = finalPoses[index]; if (shapeInfo.points.size() > 0) { - for (int j = 0; j < shapeInfo.points.size(); ++j) { + for (size_t j = 0; j < shapeInfo.points.size(); ++j) { totalExtents.addPoint((pose * shapeInfo.points[j])); } } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 7e1504e461..ca55635250 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -75,9 +75,10 @@ public: AnimPose secondaryControllerPoses[NumSecondaryControllerTypes]; // rig space bool secondaryControllerActiveFlags[NumSecondaryControllerTypes]; bool isTalking; - float bodyCapsuleRadius; - float bodyCapsuleHalfHeight; - glm::vec3 bodyCapsuleLocalOffset; + FBXJointShapeInfo hipsShapeInfo; + FBXJointShapeInfo spineShapeInfo; + FBXJointShapeInfo spine1ShapeInfo; + FBXJointShapeInfo spine2ShapeInfo; }; struct EyeParameters { @@ -249,7 +250,8 @@ protected: void updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headMatrix); void updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnabled, bool leftArmEnabled, bool rightArmEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, - float bodyCapsuleRadius, float bodyCapsuleHalfHeight, const glm::vec3& bodyCapsuleLocalOffset); + const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, + const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo); void updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose); void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAt, const glm::vec3& saccade); @@ -257,6 +259,8 @@ protected: glm::vec3 calculateElbowPoleVector(int handIndex, int elbowIndex, int armIndex, int hipsIndex, bool isLeft) const; glm::vec3 calculateKneePoleVector(int footJointIndex, int kneeJoint, int upLegIndex, int hipsIndex, const AnimPose& targetFootPose) const; + glm::vec3 deflectHandFromTorso(const glm::vec3& handPosition, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, + const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) const; AnimPose _modelOffset; // model to rig space AnimPose _geometryOffset; // geometry to model space (includes unit offset & fst offsets) diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0e936c49e0..322dbe9be5 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -544,7 +544,7 @@ public: Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID); - void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } + Q_INVOKABLE void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; } // key state void setKeyState(KeyState s) { _keyState = s; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 6d4f586c52..cd313dbd05 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1682,8 +1682,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS int newIndex = it.value(); // remember vertices with at least 1/4 weight - const float EXPANSION_WEIGHT_THRESHOLD = 0.99f; - if (weight > EXPANSION_WEIGHT_THRESHOLD) { + const float EXPANSION_WEIGHT_THRESHOLD = 0.25f; + if (weight >= EXPANSION_WEIGHT_THRESHOLD) { // transform to joint-frame and save for later const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(newIndex)); points.push_back(extractTranslation(vertexTransform) * clusterScale); @@ -1788,6 +1788,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS avgPoint += points[j]; } avgPoint /= (float)points.size(); + joint.shapeInfo.avgPoint = avgPoint; // compute a k-Dop bounding volume for (uint32_t j = 0; j < cardinalDirections.size(); ++j) { @@ -1803,8 +1804,11 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } joint.shapeInfo.points.push_back(avgPoint + maxDot * cardinalDirections[j]); + joint.shapeInfo.dots.push_back(maxDot); joint.shapeInfo.points.push_back(avgPoint + minDot * cardinalDirections[j]); + joint.shapeInfo.dots.push_back(-minDot); } + generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines); } } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index f73088e7a1..170bbbf366 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -56,7 +56,10 @@ public: struct FBXJointShapeInfo { // same units and frame as FBXJoint.translation - QVector points; + glm::vec3 avgPoint; + std::vector dots; + std::vector points; + std::vector debugLines; }; /// A single joint (transformation node) extracted from an FBX document. diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index e03ec5e771..1d094d923c 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -167,6 +167,10 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { newPermissions.can(NodePermissions::Permission::canKick)) { emit canKickChanged(_permissions.can(NodePermissions::Permission::canKick)); } + if (originalPermissions.can(NodePermissions::Permission::canReplaceDomainContent) != + newPermissions.can(NodePermissions::Permission::canReplaceDomainContent)) { + emit canReplaceContentChanged(_permissions.can(NodePermissions::Permission::canReplaceDomainContent)); + } } void LimitedNodeList::setSocketLocalPort(quint16 socketLocalPort) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 5d602cc0c0..f4ec47636b 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -115,7 +115,8 @@ public: bool getThisNodeCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } bool getThisNodeCanWriteAssets() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } - + bool getThisNodeCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } + quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } Q_INVOKABLE void setSocketLocalPort(quint16 socketLocalPort); @@ -329,6 +330,7 @@ signals: void canRezTmpChanged(bool canRezTmp); void canWriteAssetsChanged(bool canWriteAssets); void canKickChanged(bool canKick); + void canReplaceContentChanged(bool canReplaceContent); protected slots: void connectedForLocalSocketTest(); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 4d09f077bd..4451ba4abe 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -74,6 +74,7 @@ public: bool getCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } bool getCanWriteToAssetServer() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } + bool getCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } void parseIgnoreRequestMessage(QSharedPointer message); void addIgnoredNode(const QUuid& otherNodeID); diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index a1d4fc182e..cc5df515aa 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -45,6 +45,7 @@ NodePermissions::NodePermissions(QMap perms) { permissions |= perms["id_can_connect_past_max_capacity"].toBool() ? Permission::canConnectPastMaxCapacity : Permission::none; permissions |= perms["id_can_kick"].toBool() ? Permission::canKick : Permission::none; + permissions |= perms["id_can_replace_content"].toBool() ? Permission::canReplaceDomainContent : Permission::none; } QVariant NodePermissions::toVariant(QHash groupRanks) { @@ -65,6 +66,7 @@ QVariant NodePermissions::toVariant(QHash groupRanks) { values["id_can_write_to_asset_server"] = can(Permission::canWriteToAssetServer); values["id_can_connect_past_max_capacity"] = can(Permission::canConnectPastMaxCapacity); values["id_can_kick"] = can(Permission::canKick); + values["id_can_replace_content"] = can(Permission::canReplaceDomainContent); return QVariant(values); } @@ -128,6 +130,9 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) { if (perms.can(NodePermissions::Permission::canKick)) { debug << " kick"; } + if (perms.can(NodePermissions::Permission::canReplaceDomainContent)) { + debug << " can_replace_content"; + } debug.nospace() << "]"; return debug.nospace(); } diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 6fa005e360..129d7e5c08 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -77,7 +77,8 @@ public: canRezTemporaryEntities = 8, canWriteToAssetServer = 16, canConnectPastMaxCapacity = 32, - canKick = 64 + canKick = 64, + canReplaceDomainContent = 128 }; Q_DECLARE_FLAGS(Permissions, Permission) Permissions permissions; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e2304e62f7..3314e69d78 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -121,6 +121,7 @@ public: ReplicatedAvatarIdentity, ReplicatedKillAvatar, ReplicatedBulkAvatarData, + OctreeFileReplacementFromUrl, NUM_PACKET_TYPE }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 34557e7b75..856b6dceab 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1743,8 +1743,12 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glowIntensity = 0.0f; #endif - if (glowIntensity <= 0) { - bindSimpleProgram(batch, false, false, false, true, false); + if (glowIntensity <= 0.0f) { + if (color.a >= 1.0f) { + bindSimpleProgram(batch, false, false, false, true, true); + } else { + bindSimpleProgram(batch, false, true, false, true, true); + } renderLine(batch, p1, p2, color, id); return; } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 4a9d0f570d..0ee7520122 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -371,7 +371,7 @@ void DrawOverlay3D::run(const RenderContextPointer& renderContext, const Inputs& if (_opaquePass) { gpu::doInBatch(args->_context, [&](gpu::Batch& batch){ batch.enableStereo(false); - batch.clearFramebuffer(gpu::Framebuffer::BUFFER_DEPTH, glm::vec4(), 1.f, 0, true); + batch.clearFramebuffer(gpu::Framebuffer::BUFFER_DEPTH, glm::vec4(), 1.f, 0, false); }); } diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index 4ae907eb3b..6b9718fbb8 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -17,6 +17,7 @@ #include #include "NumericalConstants.h" +#include "GLMHelpers.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 @@ -657,3 +658,150 @@ bool findPlaneFromPoints(const glm::vec3* points, size_t numPoints, glm::vec3& p planeNormalOut = glm::normalize(dir); return true; } + +bool findIntersectionOfThreePlanes(const glm::vec4& planeA, const glm::vec4& planeB, const glm::vec4& planeC, glm::vec3& intersectionPointOut) { + glm::vec3 normalA(planeA); + glm::vec3 normalB(planeB); + glm::vec3 normalC(planeC); + glm::vec3 u = glm::cross(normalB, normalC); + float denom = glm::dot(normalA, u); + if (fabsf(denom) < EPSILON) { + return false; // planes do not intersect in a point. + } else { + intersectionPointOut = (planeA.w * u + glm::cross(normalA, planeC.w * normalB - planeB.w * normalC)) / denom; + return true; + } +} + +const float INV_SQRT_3 = 1.0f / sqrtf(3.0f); +const int DOP14_COUNT = 14; +const glm::vec3 DOP14_NORMALS[DOP14_COUNT] = { + Vectors::UNIT_X, + -Vectors::UNIT_X, + Vectors::UNIT_Y, + -Vectors::UNIT_Y, + Vectors::UNIT_Z, + -Vectors::UNIT_Z, + glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), + -glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), + glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), + -glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), + glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), + -glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), + glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3), + -glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3) +}; + +typedef std::tuple Int3Tuple; +const std::tuple DOP14_PLANE_COMBINATIONS[] = { + Int3Tuple(0, 2, 4), Int3Tuple(0, 2, 5), Int3Tuple(0, 2, 6), Int3Tuple(0, 2, 7), Int3Tuple(0, 2, 8), Int3Tuple(0, 2, 9), Int3Tuple(0, 2, 10), Int3Tuple(0, 2, 11), Int3Tuple(0, 2, 12), Int3Tuple(0, 2, 13), + Int3Tuple(0, 3, 4), Int3Tuple(0, 3, 5), Int3Tuple(0, 3, 6), Int3Tuple(0, 3, 7), Int3Tuple(0, 3, 8), Int3Tuple(0, 3, 9), Int3Tuple(0, 3, 10), Int3Tuple(0, 3, 11), Int3Tuple(0, 3, 12), Int3Tuple(0, 3, 13), + Int3Tuple(0, 4, 6), Int3Tuple(0, 4, 7), Int3Tuple(0, 4, 8), Int3Tuple(0, 4, 9), Int3Tuple(0, 4, 10), Int3Tuple(0, 4, 11), Int3Tuple(0, 4, 12), Int3Tuple(0, 4, 13), + Int3Tuple(0, 5, 6), Int3Tuple(0, 5, 7), Int3Tuple(0, 5, 8), Int3Tuple(0, 5, 9), Int3Tuple(0, 5, 10), Int3Tuple(0, 5, 11), Int3Tuple(0, 5, 12), Int3Tuple(0, 5, 13), + Int3Tuple(0, 6, 8), Int3Tuple(0, 6, 9), Int3Tuple(0, 6, 10), Int3Tuple(0, 6, 11), Int3Tuple(0, 6, 12), Int3Tuple(0, 6, 13), + Int3Tuple(0, 7, 8), Int3Tuple(0, 7, 9), Int3Tuple(0, 7, 10), Int3Tuple(0, 7, 11), Int3Tuple(0, 7, 12), Int3Tuple(0, 7, 13), + Int3Tuple(0, 8, 10), Int3Tuple(0, 8, 11), Int3Tuple(0, 8, 12), Int3Tuple(0, 8, 13), Int3Tuple(0, 9, 10), + Int3Tuple(0, 9, 11), Int3Tuple(0, 9, 12), Int3Tuple(0, 9, 13), + Int3Tuple(0, 10, 12), Int3Tuple(0, 10, 13), + Int3Tuple(0, 11, 12), Int3Tuple(0, 11, 13), + Int3Tuple(1, 2, 4), Int3Tuple(1, 2, 5), Int3Tuple(1, 2, 6), Int3Tuple(1, 2, 7), Int3Tuple(1, 2, 8), Int3Tuple(1, 2, 9), Int3Tuple(1, 2, 10), Int3Tuple(1, 2, 11), Int3Tuple(1, 2, 12), Int3Tuple(1, 2, 13), + Int3Tuple(1, 3, 4), Int3Tuple(1, 3, 5), Int3Tuple(1, 3, 6), Int3Tuple(1, 3, 7), Int3Tuple(1, 3, 8), Int3Tuple(1, 3, 9), Int3Tuple(1, 3, 10), Int3Tuple(1, 3, 11), Int3Tuple(1, 3, 12), Int3Tuple(1, 3, 13), + Int3Tuple(1, 4, 6), Int3Tuple(1, 4, 7), Int3Tuple(1, 4, 8), Int3Tuple(1, 4, 9), Int3Tuple(1, 4, 10), Int3Tuple(1, 4, 11), Int3Tuple(1, 4, 12), Int3Tuple(1, 4, 13), + Int3Tuple(1, 5, 6), Int3Tuple(1, 5, 7), Int3Tuple(1, 5, 8), Int3Tuple(1, 5, 9), Int3Tuple(1, 5, 10), Int3Tuple(1, 5, 11), Int3Tuple(1, 5, 12), Int3Tuple(1, 5, 13), + Int3Tuple(1, 6, 8), Int3Tuple(1, 6, 9), Int3Tuple(1, 6, 10), Int3Tuple(1, 6, 11), Int3Tuple(1, 6, 12), Int3Tuple(1, 6, 13), + Int3Tuple(1, 7, 8), Int3Tuple(1, 7, 9), Int3Tuple(1, 7, 10), Int3Tuple(1, 7, 11), Int3Tuple(1, 7, 12), Int3Tuple(1, 7, 13), + Int3Tuple(1, 8, 10), Int3Tuple(1, 8, 11), Int3Tuple(1, 8, 12), Int3Tuple(1, 8, 13), + Int3Tuple(1, 9, 10), Int3Tuple(1, 9, 11), Int3Tuple(1, 9, 12), Int3Tuple(1, 9, 13), + Int3Tuple(1, 10, 12), Int3Tuple(1, 10, 13), + Int3Tuple(1, 11, 12), Int3Tuple(1, 11, 13), + Int3Tuple(2, 4, 6), Int3Tuple(2, 4, 7), Int3Tuple(2, 4, 8), Int3Tuple(2, 4, 9), Int3Tuple(2, 4, 10), Int3Tuple(2, 4, 11), Int3Tuple(2, 4, 12), Int3Tuple(2, 4, 13), + Int3Tuple(2, 5, 6), Int3Tuple(2, 5, 7), Int3Tuple(2, 5, 8), Int3Tuple(2, 5, 9), Int3Tuple(2, 5, 10), Int3Tuple(2, 5, 11), Int3Tuple(2, 5, 12), Int3Tuple(2, 5, 13), + Int3Tuple(2, 6, 8), Int3Tuple(2, 6, 9), Int3Tuple(2, 6, 10), Int3Tuple(2, 6, 11), Int3Tuple(2, 6, 12), Int3Tuple(2, 6, 13), + Int3Tuple(2, 7, 8), Int3Tuple(2, 7, 9), Int3Tuple(2, 7, 10), Int3Tuple(2, 7, 11), Int3Tuple(2, 7, 12), Int3Tuple(2, 7, 13), + Int3Tuple(2, 8, 10), Int3Tuple(2, 8, 11), Int3Tuple(2, 8, 12), Int3Tuple(2, 8, 13), + Int3Tuple(2, 9, 10), Int3Tuple(2, 9, 11), Int3Tuple(2, 9, 12), Int3Tuple(2, 9, 13), + Int3Tuple(2, 10, 12), Int3Tuple(2, 10, 13), + Int3Tuple(2, 11, 12), Int3Tuple(2, 11, 13), + Int3Tuple(3, 4, 6), Int3Tuple(3, 4, 7), Int3Tuple(3, 4, 8), Int3Tuple(3, 4, 9), Int3Tuple(3, 4, 10), Int3Tuple(3, 4, 11), Int3Tuple(3, 4, 12), Int3Tuple(3, 4, 13), + Int3Tuple(3, 5, 6), Int3Tuple(3, 5, 7), Int3Tuple(3, 5, 8), Int3Tuple(3, 5, 9), Int3Tuple(3, 5, 10), Int3Tuple(3, 5, 11), Int3Tuple(3, 5, 12), Int3Tuple(3, 5, 13), + Int3Tuple(3, 6, 8), Int3Tuple(3, 6, 9), Int3Tuple(3, 6, 10), Int3Tuple(3, 6, 11), Int3Tuple(3, 6, 12), Int3Tuple(3, 6, 13), + Int3Tuple(3, 7, 8), Int3Tuple(3, 7, 9), Int3Tuple(3, 7, 10), Int3Tuple(3, 7, 11), Int3Tuple(3, 7, 12), Int3Tuple(3, 7, 13), + Int3Tuple(3, 8, 10), Int3Tuple(3, 8, 11), Int3Tuple(3, 8, 12), Int3Tuple(3, 8, 13), + Int3Tuple(3, 9, 10), Int3Tuple(3, 9, 11), Int3Tuple(3, 9, 12), Int3Tuple(3, 9, 13), + Int3Tuple(3, 10, 12), Int3Tuple(3, 10, 13), + Int3Tuple(3, 11, 12), Int3Tuple(3, 11, 13), + Int3Tuple(4, 6, 8), Int3Tuple(4, 6, 9), Int3Tuple(4, 6, 10), Int3Tuple(4, 6, 11), Int3Tuple(4, 6, 12), Int3Tuple(4, 6, 13), + Int3Tuple(4, 7, 8), Int3Tuple(4, 7, 9), Int3Tuple(4, 7, 10), Int3Tuple(4, 7, 11), Int3Tuple(4, 7, 12), Int3Tuple(4, 7, 13), + Int3Tuple(4, 8, 10), Int3Tuple(4, 8, 11), Int3Tuple(4, 8, 12), Int3Tuple(4, 8, 13), + Int3Tuple(4, 9, 10), Int3Tuple(4, 9, 11), Int3Tuple(4, 9, 12), Int3Tuple(4, 9, 13), + Int3Tuple(4, 10, 12), Int3Tuple(4, 10, 13), + Int3Tuple(4, 11, 12), Int3Tuple(4, 11, 13), + Int3Tuple(5, 6, 8), Int3Tuple(5, 6, 9), Int3Tuple(5, 6, 10), Int3Tuple(5, 6, 11), Int3Tuple(5, 6, 12), Int3Tuple(5, 6, 13), + Int3Tuple(5, 7, 8), Int3Tuple(5, 7, 9), Int3Tuple(5, 7, 10), Int3Tuple(5, 7, 11), Int3Tuple(5, 7, 12), Int3Tuple(5, 7, 13), + Int3Tuple(5, 8, 10), Int3Tuple(5, 8, 11), Int3Tuple(5, 8, 12), Int3Tuple(5, 8, 13), + Int3Tuple(5, 9, 10), Int3Tuple(5, 9, 11), Int3Tuple(5, 9, 12), Int3Tuple(5, 9, 13), + Int3Tuple(5, 10, 12), Int3Tuple(5, 10, 13), + Int3Tuple(5, 11, 12), Int3Tuple(5, 11, 13), + Int3Tuple(6, 8, 10), Int3Tuple(6, 8, 11), Int3Tuple(6, 8, 12), Int3Tuple(6, 8, 13), + Int3Tuple(6, 9, 10), Int3Tuple(6, 9, 11), Int3Tuple(6, 9, 12), Int3Tuple(6, 9, 13), + Int3Tuple(6, 10, 12), Int3Tuple(6, 10, 13), + Int3Tuple(6, 11, 12), Int3Tuple(6, 11, 13), + Int3Tuple(7, 8, 10), Int3Tuple(7, 8, 11), Int3Tuple(7, 8, 12), Int3Tuple(7, 8, 13), + Int3Tuple(7, 9, 10), Int3Tuple(7, 9, 11), Int3Tuple(7, 9, 12), Int3Tuple(7, 9, 13), + Int3Tuple(7, 10, 12), Int3Tuple(7, 10, 13), + Int3Tuple(7, 11, 12), Int3Tuple(7, 11, 13), + Int3Tuple(8, 10, 12), Int3Tuple(8, 10, 13), + Int3Tuple(8, 11, 12), Int3Tuple(8, 11, 13), + Int3Tuple(9, 10, 12), Int3Tuple(9, 10, 13), + Int3Tuple(9, 11, 12), Int3Tuple(9, 11, 13) +}; + +void generateBoundryLinesForDop14(const std::vector& dots, const glm::vec3& center, std::vector& linesOut) { + if (dots.size() != DOP14_COUNT) { + return; + } + + // iterate over all purmutations of non-parallel planes. + // find all the vertices that lie on the surface of the k-dop + std::vector vertices; + for (auto& tuple : DOP14_PLANE_COMBINATIONS) { + int i = std::get<0>(tuple); + int j = std::get<1>(tuple); + int k = std::get<2>(tuple); + glm::vec4 planeA(DOP14_NORMALS[i], dots[i]); + glm::vec4 planeB(DOP14_NORMALS[j], dots[j]); + glm::vec4 planeC(DOP14_NORMALS[k], dots[k]); + glm::vec3 intersectionPoint; + const float IN_FRONT_MARGIN = 0.01f; + if (findIntersectionOfThreePlanes(planeA, planeB, planeC, intersectionPoint)) { + bool inFront = false; + for (int p = 0; p < DOP14_COUNT; p++) { + if (glm::dot(DOP14_NORMALS[p], intersectionPoint) > dots[p] + IN_FRONT_MARGIN) { + inFront = true; + } + } + if (!inFront) { + vertices.push_back(intersectionPoint); + } + } + } + + // build a set of lines between these vertices, that also lie on the surface of the k-dop. + for (size_t i = 0; i < vertices.size(); i++) { + for (size_t j = i; j < vertices.size(); j++) { + glm::vec3 midPoint = (vertices[i] + vertices[j]) * 0.5f; + int onSurfaceCount = 0; + const float SURFACE_MARGIN = 0.01f; + for (int p = 0; p < DOP14_COUNT; p++) { + float d = glm::dot(DOP14_NORMALS[p], midPoint); + if (d > dots[p] - SURFACE_MARGIN && d < dots[p] + SURFACE_MARGIN) { + onSurfaceCount++; + } + } + if (onSurfaceCount > 1) { + linesOut.push_back(vertices[i] + center); + linesOut.push_back(vertices[j] + center); + } + } + } +} diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index a5ee67748b..eb9424d938 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -13,6 +13,7 @@ #define hifi_GeometryUtil_h #include +#include glm::vec3 computeVectorFromPointToSegment(const glm::vec3& point, const glm::vec3& start, const glm::vec3& end); @@ -166,4 +167,10 @@ private: // given a set of points, compute a best fit plane that passes as close as possible through all the points. bool findPlaneFromPoints(const glm::vec3* points, size_t numPoints, glm::vec3& planeNormalOut, glm::vec3& pointOnPlaneOut); +// plane equation is specified by ax + by + cz + d = 0. +// the coefficents are passed in as a vec4. (a, b, c, d) +bool findIntersectionOfThreePlanes(const glm::vec4& planeA, const glm::vec4& planeB, const glm::vec4& planeC, glm::vec3& intersectionPointOut); + +void generateBoundryLinesForDop14(const std::vector& dots, const glm::vec3& center, std::vector& linesOut); + #endif // hifi_GeometryUtil_h diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index a9b02e44dd..80c8f8a0e4 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -89,13 +89,32 @@ }); } + function addInventoryButton() { + // Why isn't this an id?! This really shouldn't be a class on the website, but it is. + var navbarBrandElement = document.getElementsByClassName('navbar-brand')[0]; + var inventoryElement = document.createElement('a'); + inventoryElement.classList.add("btn"); + inventoryElement.classList.add("btn-default"); + inventoryElement.id = "inventoryButton"; + inventoryElement.setAttribute('href', "#"); + inventoryElement.innerHTML = "INVENTORY"; + inventoryElement.style = "height:100%;margin-top:0;padding:15px 15px;"; + navbarBrandElement.parentNode.insertAdjacentElement('beforeend', inventoryElement); + $('#inventoryButton').on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "INVENTORY", + referrerURL: window.location.href + })); + }); + } + function buyButtonClicked(id, name, author, price, href) { EventBridge.emitWebEvent(JSON.stringify({ type: "CHECKOUT", itemId: id, itemName: name, itemAuthor: author, - itemPrice: price, + itemPrice: Math.round(Math.random() * 50), itemHref: href })); } @@ -132,7 +151,8 @@ // Try this here in case it works (it will if the user just pressed the "back" button, // since that doesn't trigger another AJAX request. - injectBuyButtonOnMainPage(); + injectBuyButtonOnMainPage; + addInventoryButton(); } } @@ -148,6 +168,7 @@ 10, href); }); + addInventoryButton(); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 97ed271009..9378a1d95b 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -20,6 +20,7 @@ var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/Checkout.qml"; + var MARKETPLACE_INVENTORY_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/Inventory.qml"; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; @@ -86,7 +87,7 @@ function onScreenChanged(type, url) { onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL; - wireEventBridge(type === "QML" && url === MARKETPLACE_CHECKOUT_QML_PATH); + wireEventBridge(type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_INVENTORY_QML_PATH)); // for toolbar mode: change button to active when window is first openend, false otherwise. marketplaceButton.editProperties({ isActive: onMarketplaceScreen }); if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { @@ -139,6 +140,12 @@ action: "inspectionModeSetting", data: Settings.getValue("inspectionMode", false) })); + } else if (parsedJsonMessage.type === "INVENTORY") { + tablet.pushOntoStack(MARKETPLACE_INVENTORY_QML_PATH); + tablet.sendToQml({ + method: 'updateInventory', + referrerURL: parsedJsonMessage.referrerURL + }); } } } @@ -197,19 +204,21 @@ // I don't think this is trivial to do since we also want to inject some JS into the DOM. //tablet.popFromStack(); break; - case 'checkout_buyClicked': - if (message.success === true) { - tablet.gotoWebScreen(message.itemHref); - Script.setTimeout(function () { - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); - }, 100); - } else { - tablet.sendToQml({ method: 'buyFailed' }); - } + case 'checkout_buySuccess': + tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); //tablet.popFromStack(); break; + case 'inventory_itemClicked': + var itemId = message.itemId; + if (itemId && itemId !== "") { + tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + itemId, MARKETPLACES_INJECT_SCRIPT_URL); + } + break; + case 'inventory_backClicked': + tablet.gotoWebScreen(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); + break; default: - print('Unrecognized message from Checkout.qml: ' + JSON.stringify(message)); + print('Unrecognized message from Checkout.qml or Inventory.qml: ' + JSON.stringify(message)); } } diff --git a/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js b/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js new file mode 100644 index 0000000000..e7a135ec9e --- /dev/null +++ b/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js @@ -0,0 +1,118 @@ +"use strict"; + +// +// skyboxchanger.js +// +// Created by Cain Kilgore on 9th August 2017 +// Copyright 2017 High Fidelity, Inc. +// +// 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 TABLET_BUTTON_NAME = "SKYBOX"; + + var ICONS = { + icon: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxedit-i.svg", + activeIcon: "http://mpassets.highfidelity.com/05904016-8f7d-4dfc-88e1-2bf9ba3fac20-v1/skyboxedit-i.svg" + }; + + var onSkyboxChangerScreen = false; + + function onClicked() { + if (onSkyboxChangerScreen) { + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLSource("../SkyboxChanger.qml"); + } + } + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: ICONS.icon, + activeIcon: ICONS.activeIcon, + text: TABLET_BUTTON_NAME, + sortOrder: 1 + }); + + var hasEventBridge = false; + + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + function onScreenChanged(type, url) { + if (url === "../SkyboxChanger.qml") { + onSkyboxChangerScreen = true; + } else { + onSkyboxChangerScreen = false; + } + + button.editProperties({isActive: onSkyboxChangerScreen}); + wireEventBridge(onSkyboxChangerScreen); + } + + function fromQml(message) { + switch (message.method) { + case 'changeSkybox': // changeSkybox Code + var standingZone; + if (!Entities.canRez()) { + Window.alert("You need to have rez permissions to change the Skybox."); + break; + } + + var nearbyEntities = Entities.findEntities(MyAvatar.position, 5); + for (var i = 0; i < nearbyEntities.length; i++) { + if (Entities.getEntityProperties(nearbyEntities[i]).type === "Zone") { + standingZone = nearbyEntities[i]; + } + } + + if (Entities.getEntityProperties(standingZone).locked) { + Window.alert("This zone is currently locked; the Skybox can't be changed."); + break; + } + + var newSkybox = { + skybox: { + url: message.url + }, + keyLight: { + ambientURL: message.url + } + }; + + Entities.editEntity(standingZone, newSkybox); + break; + default: + print('Unrecognized message from QML: ' + JSON.stringify(message)); + } + } + + button.clicked.connect(onClicked); + tablet.screenChanged.connect(onScreenChanged); + + Script.scriptEnding.connect(function () { + if (onSkyboxChangerScreen) { + tablet.gotoHomeScreen(); + } + button.clicked.disconnect(onClicked); + tablet.screenChanged.disconnect(onScreenChanged); + tablet.removeButton(button); + }); +}()); \ No newline at end of file