Merge branch 'master' of https://github.com/highfidelity/hifi into removeLogSpam

Conflicts:
	libraries/script-engine/src/FileScriptingInterface.cpp
This commit is contained in:
Brad Hefta-Gaub 2016-12-19 22:27:24 -08:00
commit c65471e880
45 changed files with 2139 additions and 513 deletions

View file

@ -17,7 +17,10 @@
var KEYBOARD_HEIGHT = 200;
function shouldRaiseKeyboard() {
if (document.activeElement.nodeName === "INPUT" || document.activeElement.nodeName === "TEXTAREA") {
var nodeName = document.activeElement.nodeName;
var nodeType = document.activeElement.type;
if (nodeName === "INPUT" && (nodeType === "text" || nodeType === "number" || nodeType === "password")
|| document.activeElement.nodeName === "TEXTAREA") {
return true;
} else {
// check for contenteditable attribute

View file

@ -23,6 +23,8 @@ ScrollingWindow {
property alias eventBridge: eventBridgeWrapper.eventBridge
signal loadingChanged(int status)
x: 100
y: 100
@ -44,6 +46,10 @@ ScrollingWindow {
hidePermissionsBar();
}
function setAutoAdd(auto) {
desktop.setAutoAdd(auto);
}
Item {
id:item
width: pane.contentWidth
@ -197,7 +203,7 @@ ScrollingWindow {
WebView {
id: webview
url: "https://highfidelity.com"
url: "https://highfidelity.com/"
property alias eventBridgeWrapper: eventBridgeWrapper
@ -243,6 +249,7 @@ ScrollingWindow {
if (loadRequest.status === WebEngineView.LoadSucceededStatus) {
addressBar.text = loadRequest.url
}
root.loadingChanged(loadRequest.status);
}
onIconChanged: {
@ -254,7 +261,7 @@ ScrollingWindow {
}
Component.onCompleted: {
desktop.initWebviewProfileHandlers(webview.profile)
desktop.initWebviewProfileHandlers(webview.profile);
}
}

View file

@ -1,167 +0,0 @@
//
// Marketplaces.qml
//
// Created by Elisa Lupin-Jimenez on 3 Aug 2016
// Copyright 2016 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
import QtWebChannel 1.0
import QtWebEngine 1.1
import QtWebSockets 1.0
import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel
import "controls"
import "controls-uit" as Controls
import "styles"
import "styles-uit"
Rectangle {
HifiConstants { id: hifi }
id: marketplace
anchors.fill: parent
property var marketplacesUrl: "../../scripts/system/html/marketplaces.html"
property int statusBarHeight: 50
property int statusMargin: 50
property string standardMessage: "Check out other marketplaces."
property string claraMessage: "Choose a model and click Download -> Autodesk FBX."
property string claraError: "High Fidelity only supports Autodesk FBX models."
Controls.BaseWebView {
id: webview
url: marketplacesUrl
anchors.top: marketplace.top
width: parent.width
height: parent.height - statusBarHeight
focus: true
Timer {
id: zipTimer
running: false
repeat: false
interval: 1500
property var handler;
onTriggered: handler();
}
Timer {
id: alertTimer
running: false
repeat: false
interval: 9000
property var handler;
onTriggered: handler();
}
property var autoCancel: 'var element = $("a.btn.cancel");
element.click();'
property var simpleDownload: 'var element = $("a.download-file");
element.removeClass("download-file");
element.removeAttr("download");'
function displayErrorStatus() {
alertTimer.handler = function() {
statusLabel.text = claraMessage;
statusBar.color = hifi.colors.blueHighlight;
statusIcon.text = hifi.glyphs.info;
}
alertTimer.start();
}
property var notFbxHandler: 'var element = $("a.btn.btn-primary.viewer-button.download-file")
element.click();'
// this code is for removing other file types from Clara.io's download options
//property var checkFileType: "$('[data-extension]:not([data-extension=\"fbx\"])').parent().remove()"
onLinkHovered: {
desktop.currentUrl = hoveredUrl;
//runJavaScript(checkFileType, function(){console.log("Remove filetypes JS injection");});
if (File.isZippedFbx(desktop.currentUrl)) {
runJavaScript(simpleDownload, function(){console.log("Download JS injection");});
return;
}
if (File.isZipped(desktop.currentUrl)) {
statusLabel.text = claraError;
statusBar.color = hifi.colors.redHighlight;
statusIcon.text = hifi.glyphs.alert;
runJavaScript(notFbxHandler, displayErrorStatus());
}
}
onLoadingChanged: {
if (File.isClaraLink(webview.url)) {
statusLabel.text = claraMessage;
} else {
statusLabel.text = standardMessage;
}
statusBar.color = hifi.colors.blueHighlight;
statusIcon.text = hifi.glyphs.info;
}
onNewViewRequested: {
var component = Qt.createComponent("Browser.qml");
var newWindow = component.createObject(desktop);
request.openIn(newWindow.webView);
if (File.isZippedFbx(desktop.currentUrl)) {
runJavaScript(autoCancel);
zipTimer.handler = function() {
newWindow.destroy();
}
zipTimer.start();
}
}
}
Rectangle {
id: statusBar
anchors.top: webview.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: hifi.colors.blueHighlight
Controls.Button {
id: switchMarketView
anchors.right: parent.right
anchors.rightMargin: statusMargin
anchors.verticalCenter: parent.verticalCenter
width: 150
text: "See all markets"
onClicked: {
webview.url = "../../scripts/system/html/marketplaces.html";
statusLabel.text = standardMessage;
}
}
Controls.Label {
id: statusLabel
anchors.verticalCenter: switchMarketView.verticalCenter
anchors.left: parent.left
anchors.leftMargin: statusMargin
color: hifi.colors.white
text: standardMessage
size: 18
}
HiFiGlyphs {
id: statusIcon
anchors.right: statusLabel.left
anchors.verticalCenter: statusLabel.verticalCenter
text: hifi.glyphs.info
color: hifi.colors.white
size: hifi.fontSizes.tableHeadingIcon
}
}
}

View file

@ -27,6 +27,7 @@ Windows.ScrollingWindow {
destroyOnCloseButton: false
property alias source: webview.url
property alias eventBridge: eventBridgeWrapper.eventBridge;
property alias scriptUrl: webview.userScriptUrl
QtObject {
id: eventBridgeWrapper
@ -71,6 +72,8 @@ Windows.ScrollingWindow {
focus: true
webChannel.registeredObjects: [eventBridgeWrapper]
property string userScriptUrl: ""
// Create a global EventBridge object for raiseAndLowerKeyboard.
WebEngineScript {
id: createGlobalEventBridge
@ -87,7 +90,25 @@ Windows.ScrollingWindow {
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
// User script.
WebEngineScript {
id: userScript
sourceUrl: webview.userScriptUrl
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
function onWebEventReceived(event) {
if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") {
ApplicationInterface.addAssetToWorldFromURL(event.slice(18));
}
}
Component.onCompleted: {
eventBridge.webEventReceived.connect(onWebEventReceived);
}
}
}
}

View file

@ -2,9 +2,8 @@
import QtQuick 2.3
import QtQuick.Controls 1.4
import QtWebChannel 1.0
import QtWebEngine 1.1
import QtWebEngine 1.2
import QtWebSockets 1.0
import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel
import "windows" as Windows
import "controls"
@ -23,10 +22,22 @@ Windows.Window {
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
destroyOnCloseButton: false
property var source;
property var eventBridge;
property var component;
property var dynamicContent;
// Keyboard control properties in case needed by QML content.
property bool keyboardEnabled: false
property bool keyboardRaised: false
property bool punctuationMode: false
// JavaScript event bridge object in case QML content includes Web content.
property alias eventBridge: eventBridgeWrapper.eventBridge;
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
onSourceChanged: {
if (dynamicContent) {
@ -72,7 +83,6 @@ Windows.Window {
}
}
Item {
id: contentHolder
anchors.fill: parent

View file

@ -0,0 +1,27 @@
//
// Web3DOverlay.qml
//
// Created by David Rowe on 16 Dec 2016.
// Copyright 2016 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
import "controls" as Controls
Controls.WebView {
function onWebEventReceived(event) {
if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") {
ApplicationInterface.addAssetToWorldFromURL(event.slice(18));
}
}
Component.onCompleted: {
eventBridge.webEventReceived.connect(onWebEventReceived);
}
}

View file

@ -6,6 +6,7 @@ import HFWebEngineProfile 1.0
Item {
property alias url: root.url
property alias scriptURL: root.userScriptUrl
property alias eventBridge: eventBridgeWrapper.eventBridge
property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false
property bool keyboardRaised: false
@ -38,6 +39,8 @@ Item {
storageName: "qmlWebEngine"
}
property string userScriptUrl: ""
// creates a global EventBridge object.
WebEngineScript {
id: createGlobalEventBridge
@ -54,7 +57,15 @@ Item {
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
// User script.
WebEngineScript {
id: userScript
sourceUrl: root.userScriptUrl
injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished.
worldId: WebEngineScript.MainWorld
}
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ]
property string newUrl: ""

View file

@ -100,21 +100,25 @@ OriginalDesktop.Desktop {
// Accept a download through the webview
property bool webViewProfileSetup: false
property string currentUrl: ""
property string downloadUrl: ""
property string adaptedPath: ""
property string tempDir: ""
property bool autoAdd: false
function initWebviewProfileHandlers(profile) {
console.log("The webview url in desktop is: " + currentUrl);
downloadUrl = currentUrl;
if (webViewProfileSetup) return;
webViewProfileSetup = true;
profile.downloadRequested.connect(function(download){
console.log("Download start: " + download.state);
adaptedPath = File.convertUrlToPath(currentUrl);
adaptedPath = File.convertUrlToPath(downloadUrl);
tempDir = File.getTempDir();
console.log("Temp dir created: " + tempDir);
download.path = tempDir + "/" + adaptedPath;
console.log("Path where object should download: " + download.path);
console.log("Auto add: " + autoAdd);
download.accept();
if (download.state === WebEngineDownloadItem.DownloadInterrupted) {
console.log("download failed to complete");
@ -123,13 +127,18 @@ OriginalDesktop.Desktop {
profile.downloadFinished.connect(function(download){
if (download.state === WebEngineDownloadItem.DownloadCompleted) {
File.runUnzip(download.path, currentUrl);
File.runUnzip(download.path, downloadUrl, autoAdd);
} else {
console.log("The download was corrupted, state: " + download.state);
}
autoAdd = false;
})
}
function setAutoAdd(auto) {
autoAdd = auto;
}
// Create or fetch a toolbar with the given name
function getToolbar(name) {
var result = toolbars[name];

View file

@ -42,6 +42,7 @@
#include <QtMultimedia/QMediaPlayer>
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include <gl/QOpenGLContextWrapper.h>
@ -54,6 +55,7 @@
#include <AnimDebugDraw.h>
#include <BuildInfo.h>
#include <AssetClient.h>
#include <AssetUpload.h>
#include <AutoUpdater.h>
#include <AudioInjectorManager.h>
#include <CursorManager.h>
@ -79,6 +81,7 @@
#include <UserActivityLoggerScriptingInterface.h>
#include <LogHandler.h>
#include <MainWindow.h>
#include <MappingRequest.h>
#include <MessagesClient.h>
#include <ModelEntityItem.h>
#include <NetworkAccessManager.h>
@ -157,6 +160,7 @@
#include "ui/DialogsManager.h"
#include "ui/LoginDialog.h"
#include "ui/overlays/Cube3DOverlay.h"
#include "ui/overlays/Web3DOverlay.h"
#include "ui/Snapshot.h"
#include "ui/SnapshotAnimated.h"
#include "ui/StandAloneJSConsole.h"
@ -1190,11 +1194,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
[this](const EntityItemID& entityItemID, const PointerEvent& event) {
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
setKeyboardFocusEntity(entityItemID);
});
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) {
if (entityItemID == _keyboardFocusedItem.get()) {
if (entityItemID == _keyboardFocusedEntity.get()) {
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
}
});
@ -1204,7 +1209,26 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
});
// Keyboard focus handling for Web overlays.
auto overlays = &(qApp->getOverlays());
connect(overlays, &Overlays::mousePressOnOverlay, [=](unsigned int overlayID, const PointerEvent& event) {
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
setKeyboardFocusOverlay(overlayID);
});
connect(overlays, &Overlays::overlayDeleted, [=](unsigned int overlayID) {
if (overlayID == _keyboardFocusedOverlay.get()) {
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
}
});
connect(overlays, &Overlays::mousePressOffOverlay, [=]() {
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
});
connect(this, &Application::aboutToQuit, [=]() {
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
});
@ -1476,6 +1500,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// After all of the constructor is completed, then set firstRun to false.
firstRun.set(false);
}
// Monitor model assets (e.g., from Clara.io) added to the world that may need resizing.
static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000;
_addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS);
connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize);
// Auto-update and close adding asset to world info message box.
static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000;
_addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS);
_addAssetToWorldInfoTimer.setSingleShot(true);
connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout);
static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000;
_addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS);
_addAssetToWorldErrorTimer.setSingleShot(true);
connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout);
connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose);
connect(&domainHandler, &DomainHandler::hostnameChanged, this, &Application::addAssetToWorldMessageClose);
}
void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) {
@ -1865,9 +1907,9 @@ void Application::initializeUi() {
rootContext->setContextProperty("AudioStats", DependencyManager::get<AudioClient>()->getStats().data());
rootContext->setContextProperty("Controller", DependencyManager::get<controller::ScriptingInterface>().data());
rootContext->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data());
FileScriptingInterface* fileDownload = new FileScriptingInterface(engine);
rootContext->setContextProperty("File", fileDownload);
connect(fileDownload, &FileScriptingInterface::unzipSuccess, this, &Application::showAssetServerWidget);
_fileDownload = new FileScriptingInterface(engine);
rootContext->setContextProperty("File", _fileDownload);
connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip);
rootContext->setContextProperty("MyAvatar", getMyAvatar().get());
rootContext->setContextProperty("Messages", DependencyManager::get<MessagesClient>().data());
rootContext->setContextProperty("Recording", DependencyManager::get<RecordingScriptingInterface>().data());
@ -2395,26 +2437,49 @@ bool Application::event(QEvent* event) {
}
{
if (!_keyboardFocusedItem.get().isInvalidID()) {
if (!_keyboardFocusedEntity.get().isInvalidID()) {
switch (event->type()) {
case QEvent::KeyPress:
case QEvent::KeyRelease: {
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedItem.get());
if (entity && entity->getEventHandler()) {
event->setAccepted(false);
QCoreApplication::sendEvent(entity->getEventHandler(), event);
if (event->isAccepted()) {
_lastAcceptedKeyPress = usecTimestampNow();
return true;
case QEvent::KeyPress:
case QEvent::KeyRelease: {
//auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get());
if (entity && entity->getEventHandler()) {
event->setAccepted(false);
QCoreApplication::sendEvent(entity->getEventHandler(), event);
if (event->isAccepted()) {
_lastAcceptedKeyPress = usecTimestampNow();
return true;
}
}
break;
}
break;
}
default:
break;
}
}
}
default:
break;
}
{
if (_keyboardFocusedOverlay.get() != UNKNOWN_OVERLAY_ID) {
switch (event->type()) {
case QEvent::KeyPress:
case QEvent::KeyRelease: {
// Only Web overlays can have focus.
auto overlay =
std::dynamic_pointer_cast<Web3DOverlay>(getOverlays().getOverlay(_keyboardFocusedOverlay.get()));
if (overlay && overlay->getEventHandler()) {
event->setAccepted(false);
QCoreApplication::sendEvent(overlay->getEventHandler(), event);
if (event->isAccepted()) {
_lastAcceptedKeyPress = usecTimestampNow();
return true;
}
}
break;
}
default:
break;
}
}
}
@ -2974,6 +3039,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() ||
getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y()))) {
getOverlays().mouseMoveEvent(&mappedEvent);
getEntities()->mouseMoveEvent(&mappedEvent);
}
_controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts
@ -2986,7 +3052,6 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->mouseMoveEvent(event);
}
}
void Application::mousePressEvent(QMouseEvent* event) {
@ -3008,6 +3073,7 @@ void Application::mousePressEvent(QMouseEvent* event) {
event->buttons(), event->modifiers());
if (!_aboutToQuit) {
getOverlays().mousePressEvent(&mappedEvent);
getEntities()->mousePressEvent(&mappedEvent);
}
@ -3018,7 +3084,6 @@ void Application::mousePressEvent(QMouseEvent* event) {
return;
}
if (hasFocus()) {
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->mousePressEvent(event);
@ -3029,7 +3094,6 @@ void Application::mousePressEvent(QMouseEvent* event) {
HFActionEvent actionEvent(HFActionEvent::startType(),
computePickRay(mappedEvent.x(), mappedEvent.y()));
sendEvent(this, &actionEvent);
}
}
}
@ -3054,6 +3118,7 @@ void Application::mouseReleaseEvent(QMouseEvent* event) {
event->buttons(), event->modifiers());
if (!_aboutToQuit) {
getOverlays().mouseReleaseEvent(&mappedEvent);
getEntities()->mouseReleaseEvent(&mappedEvent);
}
@ -3293,20 +3358,30 @@ void Application::idle(float nsecsElapsed) {
}
// Drop focus from _keyboardFocusedItem if no keyboard messages for 30 seconds
// Update focus highlight for entity or overlay.
{
if (!_keyboardFocusedItem.get().isInvalidID()) {
if (!_keyboardFocusedEntity.get().isInvalidID() || _keyboardFocusedOverlay.get() != UNKNOWN_OVERLAY_ID) {
const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus
quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress;
if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) {
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
} else {
// update position of highlight overlay
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedItem.get());
if (entity && _keyboardFocusHighlight) {
_keyboardFocusHighlight->setRotation(entity->getRotation());
_keyboardFocusHighlight->setPosition(entity->getPosition());
if (!_keyboardFocusedEntity.get().isInvalidID()) {
auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get());
if (entity && _keyboardFocusHighlight) {
_keyboardFocusHighlight->setRotation(entity->getRotation());
_keyboardFocusHighlight->setPosition(entity->getPosition());
}
} else {
// Only Web overlays can have focus.
auto overlay =
std::dynamic_pointer_cast<Web3DOverlay>(getOverlays().getOverlay(_keyboardFocusedOverlay.get()));
if (overlay && _keyboardFocusHighlight) {
_keyboardFocusHighlight->setRotation(overlay->getRotation());
_keyboardFocusHighlight->setPosition(overlay->getPosition());
}
}
}
}
@ -3894,8 +3969,31 @@ void Application::rotationModeChanged() const {
}
}
void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) {
// Create focus
if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) {
_keyboardFocusHighlight = std::make_shared<Cube3DOverlay>();
_keyboardFocusHighlight->setAlpha(1.0f);
_keyboardFocusHighlight->setBorderSize(1.0f);
_keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 });
_keyboardFocusHighlight->setIsSolid(false);
_keyboardFocusHighlight->setPulseMin(0.5);
_keyboardFocusHighlight->setPulseMax(1.0);
_keyboardFocusHighlight->setColorPulse(1.0);
_keyboardFocusHighlight->setIgnoreRayIntersection(true);
_keyboardFocusHighlight->setDrawInFront(false);
_keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight);
}
// Position focus
_keyboardFocusHighlight->setRotation(rotation);
_keyboardFocusHighlight->setPosition(position);
_keyboardFocusHighlight->setDimensions(dimensions);
_keyboardFocusHighlight->setVisible(true);
}
QUuid Application::getKeyboardFocusEntity() const {
return _keyboardFocusedItem.get();
return _keyboardFocusedEntity.get();
}
void Application::setKeyboardFocusEntity(QUuid id) {
@ -3903,21 +4001,21 @@ void Application::setKeyboardFocusEntity(QUuid id) {
setKeyboardFocusEntity(entityItemID);
}
static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f;
void Application::setKeyboardFocusEntity(EntityItemID entityItemID) {
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
if (_keyboardFocusedItem.get() != entityItemID) {
// reset focused entity
_keyboardFocusedItem.set(UNKNOWN_ENTITY_ID);
if (_keyboardFocusHighlight) {
if (_keyboardFocusedEntity.get() != entityItemID) {
_keyboardFocusedEntity.set(entityItemID);
if (_keyboardFocusHighlight && _keyboardFocusedOverlay.get() == UNKNOWN_OVERLAY_ID) {
_keyboardFocusHighlight->setVisible(false);
}
// if invalid, return without expensive (locking) operations
if (entityItemID == UNKNOWN_ENTITY_ID) {
return;
}
// if valid, query properties
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
auto properties = entityScriptingInterface->getEntityProperties(entityItemID);
if (!properties.getLocked() && properties.getVisible()) {
auto entity = getEntities()->getTree()->findEntityByID(entityItemID);
@ -3926,34 +4024,49 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) {
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->pluginFocusOutEvent();
}
_keyboardFocusedItem.set(entityItemID);
_lastAcceptedKeyPress = usecTimestampNow();
// create a focus
if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) {
_keyboardFocusHighlight = std::make_shared<Cube3DOverlay>();
_keyboardFocusHighlight->setAlpha(1.0f);
_keyboardFocusHighlight->setBorderSize(1.0f);
_keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 });
_keyboardFocusHighlight->setIsSolid(false);
_keyboardFocusHighlight->setPulseMin(0.5);
_keyboardFocusHighlight->setPulseMax(1.0);
_keyboardFocusHighlight->setColorPulse(1.0);
_keyboardFocusHighlight->setIgnoreRayIntersection(true);
_keyboardFocusHighlight->setDrawInFront(false);
_keyboardFocusHighlightID = getOverlays().addOverlay(_keyboardFocusHighlight);
}
// position the focus
_keyboardFocusHighlight->setRotation(entity->getRotation());
_keyboardFocusHighlight->setPosition(entity->getPosition());
_keyboardFocusHighlight->setDimensions(entity->getDimensions() * 1.05f);
_keyboardFocusHighlight->setVisible(true);
setKeyboardFocusHighlight(entity->getPosition(), entity->getRotation(),
entity->getDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR);
}
}
}
}
unsigned int Application::getKeyboardFocusOverlay() {
return _keyboardFocusedOverlay.get();
}
void Application::setKeyboardFocusOverlay(unsigned int overlayID) {
if (overlayID != _keyboardFocusedOverlay.get()) {
_keyboardFocusedOverlay.set(overlayID);
if (_keyboardFocusHighlight && _keyboardFocusedEntity.get() == UNKNOWN_ENTITY_ID) {
_keyboardFocusHighlight->setVisible(false);
}
if (overlayID == UNKNOWN_OVERLAY_ID) {
return;
}
auto overlayType = getOverlays().getOverlayType(overlayID);
auto isVisible = getOverlays().getProperty(overlayID, "visible").value.toBool();
if (overlayType == Web3DOverlay::TYPE && isVisible) {
auto overlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlays().getOverlay(overlayID));
overlay->setProxyWindow(_window->windowHandle());
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->pluginFocusOutEvent();
}
_lastAcceptedKeyPress = usecTimestampNow();
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
const float OVERLAY_DEPTH = 0.0105f;
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
}
}
}
void Application::updateDialogs(float deltaTime) const {
PerformanceTimer perfTimer("updateDialogs");
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
@ -5515,6 +5628,464 @@ void Application::showAssetServerWidget(QString filePath) {
startUpload(nullptr, nullptr);
}
void Application::addAssetToWorldFromURL(QString url) {
qInfo(interfaceapp) << "Download asset and add to world from" << url;
QString filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL.
if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
QString errorInfo = "You do not have permissions to write to the Asset Server.";
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
addAssetToWorldError(filename, errorInfo);
return;
}
addAssetToWorldInfo(filename, "Downloading asset file " + filename + ".");
auto request = ResourceManager::createResourceRequest(nullptr, QUrl(url));
connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished);
request->send();
}
void Application::addAssetToWorldFromURLRequestFinished() {
auto request = qobject_cast<ResourceRequest*>(sender());
auto url = request->getUrl().toString();
auto result = request->getResult();
QString filename = url.section("filename=", 1, 1); // Filename from trailing "?filename=" URL parameter.
if (result == ResourceRequest::Success) {
qInfo(interfaceapp) << "Downloaded asset from" << url;
QTemporaryDir temporaryDir;
temporaryDir.setAutoRemove(false);
if (temporaryDir.isValid()) {
QString temporaryDirPath = temporaryDir.path();
QString downloadPath = temporaryDirPath + "/" + filename;
qInfo(interfaceapp) << "Download path:" << downloadPath;
QFile tempFile(downloadPath);
if (tempFile.open(QIODevice::WriteOnly)) {
tempFile.write(request->getData());
addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key.
qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true);
} else {
QString errorInfo = "Couldn't open temporary file for download";
qWarning(interfaceapp) << errorInfo;
addAssetToWorldError(filename, errorInfo);
}
} else {
QString errorInfo = "Couldn't create temporary directory for download";
qWarning(interfaceapp) << errorInfo;
addAssetToWorldError(filename, errorInfo);
}
} else {
qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString();
addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString());
}
request->deleteLater();
}
QString filenameFromPath(QString filePath) {
return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1);
}
void Application::addAssetToWorldUnzipFailure(QString filePath) {
QString filename = filenameFromPath(QUrl(filePath).toLocalFile());
qWarning(interfaceapp) << "Couldn't unzip file" << filePath;
addAssetToWorldError(filename, "Couldn't unzip file " + filename + ".");
}
void Application::addAssetToWorld(QString filePath) {
// Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget().
QString path = QUrl(filePath).toLocalFile();
QString filename = filenameFromPath(path);
QString mapping = "/" + filename;
// Test repeated because possibly different code paths.
if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
QString errorInfo = "You do not have permissions to write to the Asset Server.";
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
addAssetToWorldError(filename, errorInfo);
return;
}
addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server.");
addAssetToWorldWithNewMapping(path, mapping, 0);
}
void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy) {
auto request = DependencyManager::get<AssetClient>()->createGetMappingRequest(mapping);
QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable {
const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard.
auto result = request->getError();
if (result == GetMappingRequest::NotFound) {
addAssetToWorldUpload(filePath, mapping);
} else if (result != GetMappingRequest::NoError) {
QString errorInfo = "Could not map asset name: "
+ mapping.left(mapping.length() - QString::number(copy).length() - 1);
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
} else if (copy < MAX_COPY_COUNT - 1) {
if (copy > 0) {
mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1);
}
copy++;
mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy));
addAssetToWorldWithNewMapping(filePath, mapping, copy);
} else {
QString errorInfo = "Too many copies of asset name: "
+ mapping.left(mapping.length() - QString::number(copy).length() - 1);
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
}
request->deleteLater();
});
request->start();
}
void Application::addAssetToWorldUpload(QString filePath, QString mapping) {
qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping;
auto upload = DependencyManager::get<AssetClient>()->createUpload(filePath);
QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable {
if (upload->getError() != AssetUpload::NoError) {
QString errorInfo = "Could not upload asset to the Asset Server.";
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
} else {
addAssetToWorldSetMapping(filePath, mapping, hash);
}
// Remove temporary directory created by Clara.io market place download.
int index = filePath.lastIndexOf("/model_repo/");
if (index > 0) {
QString tempDir = filePath.left(index);
qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir;
QDir(tempDir).removeRecursively();
}
upload->deleteLater();
});
upload->start();
}
void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash) {
auto request = DependencyManager::get<AssetClient>()->createSetMappingRequest(mapping, hash);
connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable {
if (request->getError() != SetMappingRequest::NoError) {
QString errorInfo = "Could not set asset mapping.";
qWarning(interfaceapp) << "Error downloading asset: " + errorInfo;
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
} else {
addAssetToWorldAddEntity(filePath, mapping);
}
request->deleteLater();
});
request->start();
}
void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) {
EntityItemProperties properties;
properties.setType(EntityTypes::Model);
properties.setName(mapping.right(mapping.length() - 1));
properties.setModelURL("atp:" + mapping);
properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND);
properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar.
properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions.
properties.setPosition(getMyAvatar()->getPosition() + getMyAvatar()->getOrientation() * glm::vec3(0.0f, 0.0f, -2.0f));
properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f));
auto entityID = DependencyManager::get<EntityScriptingInterface>()->addEntity(properties);
// Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later
// on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted.
if (entityID == QUuid()) {
QString errorInfo = "Could not add asset " + mapping + " to world.";
qWarning(interfaceapp) << "Could not add asset to world: " + errorInfo;
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
} else {
// Monitor when asset is rendered in world so that can resize if necessary.
_addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed.
if (!_addAssetToWorldResizeTimer.isActive()) {
_addAssetToWorldResizeTimer.start();
}
// Close progress message box.
addAssetToWorldInfoDone(filenameFromPath(filePath));
}
}
void Application::addAssetToWorldCheckModelSize() {
if (_addAssetToWorldResizeList.size() == 0) {
return;
}
auto item = _addAssetToWorldResizeList.begin();
while (item != _addAssetToWorldResizeList.end()) {
auto entityID = item.key();
EntityPropertyFlags propertyFlags;
propertyFlags += PROP_NAME;
propertyFlags += PROP_DIMENSIONS;
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags);
auto name = properties.getName();
auto dimensions = properties.getDimensions();
const QString GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":true}}";
bool doResize = false;
const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f);
if (dimensions != DEFAULT_DIMENSIONS) {
// Scale model so that its maximum is exactly specific size.
const float MAXIMUM_DIMENSION = 1.0f;
auto previousDimensions = dimensions;
auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y,
MAXIMUM_DIMENSION / dimensions.z));
dimensions *= scale;
qInfo(interfaceapp) << "Asset" << name << "auto-resized from" << previousDimensions << " to " << dimensions;
doResize = true;
item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next.
} else {
// Increment count of checks done.
_addAssetToWorldResizeList[entityID]++;
const int CHECK_MODEL_SIZE_MAX_CHECKS = 300;
if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) {
// Have done enough checks; model was either the default size or something's gone wrong.
// Rescale all dimensions.
const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f);
dimensions = UNIT_DIMENSIONS;
qInfo(interfaceapp) << "Asset" << name << "auto-resize timed out; resized to " << dimensions;
doResize = true;
item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next.
} else {
// No action on this entity; advance to next.
++item;
}
}
if (doResize) {
EntityItemProperties properties;
properties.setDimensions(dimensions);
properties.setVisible(true);
properties.setCollisionless(false);
properties.setUserData(GRABBABLE_USER_DATA);
properties.setLastEdited(usecTimestampNow());
entityScriptingInterface->editEntity(entityID, properties);
}
}
// Stop timer if nothing in list to check.
if (_addAssetToWorldResizeList.size() == 0) {
_addAssetToWorldResizeTimer.stop();
}
}
void Application::addAssetToWorldInfo(QString modelName, QString infoText) {
// Displays the most recent info message, subject to being overridden by error messages.
if (_aboutToQuit) {
return;
}
/*
Cancel info timer if running.
If list has an entry for modelName, delete it (just one).
Append modelName, infoText to list.
Display infoText in message box unless an error is being displayed (i.e., error timer is running).
Show message box if not already visible.
*/
_addAssetToWorldInfoTimer.stop();
addAssetToWorldInfoClear(modelName);
_addAssetToWorldInfoKeys.append(modelName);
_addAssetToWorldInfoMessages.append(infoText);
if (!_addAssetToWorldErrorTimer.isActive()) {
if (!_addAssetToWorldMessageBox) {
_addAssetToWorldMessageBox = DependencyManager::get<OffscreenUi>()->createMessageBox(OffscreenUi::ICON_INFORMATION,
"Downloading Asset", "", QMessageBox::NoButton, QMessageBox::NoButton);
connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed()));
}
_addAssetToWorldMessageBox->setProperty("text", "\n" + infoText);
_addAssetToWorldMessageBox->setVisible(true);
}
}
void Application::addAssetToWorldInfoClear(QString modelName) {
// Clears modelName entry from message list without affecting message currently displayed.
if (_aboutToQuit) {
return;
}
/*
Delete entry for modelName from list.
*/
auto index = _addAssetToWorldInfoKeys.indexOf(modelName);
if (index > -1) {
_addAssetToWorldInfoKeys.removeAt(index);
_addAssetToWorldInfoMessages.removeAt(index);
}
}
void Application::addAssetToWorldInfoDone(QString modelName) {
// Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest.
if (_aboutToQuit) {
return;
}
/*
Delete entry for modelName from list.
(Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout()
*/
addAssetToWorldInfoClear(modelName);
_addAssetToWorldInfoTimer.start();
}
void Application::addAssetToWorldInfoTimeout() {
if (_aboutToQuit) {
return;
}
/*
If list not empty, display last message in list (may already be displayed ) unless an error is being displayed.
If list empty, close the message box unless an error is being displayed.
*/
if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) {
if (_addAssetToWorldInfoKeys.length() > 0) {
_addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last());
} else {
disconnect(_addAssetToWorldMessageBox);
_addAssetToWorldMessageBox->setVisible(false);
_addAssetToWorldMessageBox->deleteLater();
_addAssetToWorldMessageBox = nullptr;
}
}
}
void Application::addAssetToWorldError(QString modelName, QString errorText) {
// Displays the most recent error message for a few seconds.
if (_aboutToQuit) {
return;
}
/*
If list has an entry for modelName, delete it.
Display errorText in message box.
Show message box if not already visible.
(Re)start error timer. ... onAddAssetToWorldErrorTimeout()
*/
addAssetToWorldInfoClear(modelName);
if (!_addAssetToWorldMessageBox) {
_addAssetToWorldMessageBox = DependencyManager::get<OffscreenUi>()->createMessageBox(OffscreenUi::ICON_INFORMATION,
"Downloading Asset", "", QMessageBox::NoButton, QMessageBox::NoButton);
connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed()));
}
_addAssetToWorldMessageBox->setProperty("text", "\n" + errorText);
_addAssetToWorldMessageBox->setVisible(true);
_addAssetToWorldErrorTimer.start();
}
void Application::addAssetToWorldErrorTimeout() {
if (_aboutToQuit) {
return;
}
/*
If list is not empty, display message from last entry.
If list is empty, close the message box.
*/
if (_addAssetToWorldMessageBox) {
if (_addAssetToWorldInfoKeys.length() > 0) {
_addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last());
} else {
disconnect(_addAssetToWorldMessageBox);
_addAssetToWorldMessageBox->setVisible(false);
_addAssetToWorldMessageBox->deleteLater();
_addAssetToWorldMessageBox = nullptr;
}
}
}
void Application::addAssetToWorldMessageClose() {
// Clear messages, e.g., if Interface is being closed or domain changes.
/*
Call if user manually closes message box.
Call if domain changes.
Call if application is shutting down.
Stop timers.
Close the message box if open.
Clear lists.
*/
_addAssetToWorldInfoTimer.stop();
_addAssetToWorldErrorTimer.stop();
if (_addAssetToWorldMessageBox) {
disconnect(_addAssetToWorldMessageBox);
_addAssetToWorldMessageBox->setVisible(false);
_addAssetToWorldMessageBox->deleteLater();
_addAssetToWorldMessageBox = nullptr;
}
_addAssetToWorldInfoKeys.clear();
_addAssetToWorldInfoMessages.clear();
}
void Application::onAssetToWorldMessageBoxClosed() {
if (_addAssetToWorldMessageBox) {
// User manually closed message box; perhaps because it has become stuck, so reset all messages.
qInfo(interfaceapp) << "User manually closed download status message box";
disconnect(_addAssetToWorldMessageBox);
_addAssetToWorldMessageBox = nullptr;
addAssetToWorldMessageClose();
}
}
void Application::handleUnzip(QString zipFile, QString unzipFile, bool autoAdd) {
if (autoAdd) {
if (!unzipFile.isEmpty()) {
addAssetToWorld(unzipFile);
} else {
addAssetToWorldUnzipFailure(zipFile);
}
} else {
showAssetServerWidget(unzipFile);
}
}
void Application::packageModel() {
ModelPackager::package();
}

View file

@ -18,6 +18,7 @@
#include <QtCore/QPointer>
#include <QtCore/QSet>
#include <QtCore/QStringList>
#include <QtQuick/QQuickItem>
#include <QtGui/QImage>
@ -28,6 +29,7 @@
#include <AbstractViewStateInterface.h>
#include <EntityEditPacketSender.h>
#include <EntityTreeRenderer.h>
#include <FileScriptingInterface.h>
#include <input-plugins/KeyboardMouseDevice.h>
#include <input-plugins/TouchscreenDevice.h>
#include <OctreeQuery.h>
@ -310,6 +312,20 @@ public slots:
void toggleRunningScriptsWidget() const;
Q_INVOKABLE void showAssetServerWidget(QString filePath = "");
// FIXME: Move addAssetToWorld* methods to own class?
void addAssetToWorldFromURL(QString url);
void addAssetToWorldFromURLRequestFinished();
void addAssetToWorld(QString filePath);
void addAssetToWorldUnzipFailure(QString filePath);
void addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy);
void addAssetToWorldUpload(QString filePath, QString mapping);
void addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash);
void addAssetToWorldAddEntity(QString filePath, QString mapping);
void handleUnzip(QString sourceFile, QString destinationFile, bool autoAdd);
FileScriptingInterface* getFileDownloadInterface() { return _fileDownload; }
void handleLocalServerConnection() const;
void readArgumentsFromLocalSocket() const;
@ -352,10 +368,17 @@ public slots:
static void runTests();
void setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions);
QUuid getKeyboardFocusEntity() const; // thread-safe
void setKeyboardFocusEntity(QUuid id);
void setKeyboardFocusEntity(EntityItemID entityItemID);
unsigned int getKeyboardFocusOverlay();
void setKeyboardFocusOverlay(unsigned int overlayID);
void addAssetToWorldMessageClose();
private slots:
void showDesktop();
void clearDomainOctreeDetails();
@ -392,6 +415,12 @@ private slots:
void updateDisplayMode();
void domainConnectionRefused(const QString& reasonMessage, int reason, const QString& extraInfo);
void addAssetToWorldCheckModelSize();
void onAssetToWorldMessageBoxClosed();
void addAssetToWorldInfoTimeout();
void addAssetToWorldErrorTimeout();
private:
static void initDisplay();
void init();
@ -567,7 +596,8 @@ private:
DialogsManagerScriptingInterface* _dialogsManagerScriptingInterface = new DialogsManagerScriptingInterface();
ThreadSafeValueCache<EntityItemID> _keyboardFocusedItem;
ThreadSafeValueCache<EntityItemID> _keyboardFocusedEntity;
ThreadSafeValueCache<unsigned int> _keyboardFocusedOverlay;
quint64 _lastAcceptedKeyPress = 0;
bool _isForeground = true; // starts out assumed to be in foreground
bool _inPaint = false;
@ -609,6 +639,22 @@ private:
model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ;
gpu::TexturePointer _defaultSkyboxTexture;
gpu::TexturePointer _defaultSkyboxAmbientTexture;
QTimer _addAssetToWorldResizeTimer;
QHash<QUuid, int> _addAssetToWorldResizeList;
void addAssetToWorldInfo(QString modelName, QString infoText);
void addAssetToWorldInfoClear(QString modelName);
void addAssetToWorldInfoDone(QString modelName);
void addAssetToWorldError(QString modelName, QString errorText);
QQuickItem* _addAssetToWorldMessageBox{ nullptr };
QStringList _addAssetToWorldInfoKeys; // Model name
QStringList _addAssetToWorldInfoMessages; // Info message
QTimer _addAssetToWorldInfoTimer;
QTimer _addAssetToWorldErrorTimer;
FileScriptingInterface* _fileDownload;
};

View file

@ -202,9 +202,8 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec
if (_texture && _texture->isLoaded()) {
// Make sure position and rotation is updated.
Transform transform = getTransform();
// XXX this code runs too often for this...
// applyTransformTo(transform, true);
// setTransform(transform);
// Don't call applyTransformTo() or setTransform() here because this code runs too frequently.
// Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale.
bool isNull = _fromImage.isNull();

View file

@ -81,6 +81,7 @@ void ModelOverlay::render(RenderArgs* args) {
}
_model->setVisibleInScene(_visible, scene);
_model->setLayeredInFront(getDrawInFront(), scene);
scene->enqueuePendingChanges(pendingChanges);
}

View file

@ -294,6 +294,14 @@ QString Overlays::getOverlayType(unsigned int overlayId) const {
return "";
}
QObject* Overlays::getOverlayObject(unsigned int id) {
Overlay::Pointer thisOverlay = getOverlay(id);
if (thisOverlay) {
return qobject_cast<QObject*>(&(*thisOverlay));
}
return nullptr;
}
unsigned int Overlays::getParentPanel(unsigned int childId) const {
Overlay::Pointer overlay = getOverlay(childId);
auto attachable = std::dynamic_pointer_cast<PanelAttachable>(overlay);
@ -390,7 +398,7 @@ QScriptValue OverlayPropertyResultToScriptValue(QScriptEngine* engine, const Ove
if (!value.value.isValid()) {
return QScriptValue::UndefinedValue;
}
return engine->newVariant(value.value);
return engine->toScriptValue(value.value);
}
void OverlayPropertyResultFromScriptValue(const QScriptValue& object, OverlayPropertyResult& value) {
@ -598,6 +606,38 @@ bool Overlays::isAddedOverlay(unsigned int id) {
return _overlaysHUD.contains(id) || _overlaysWorld.contains(id);
}
void Overlays::sendMousePressOnOverlay(unsigned int overlayID, const PointerEvent& event) {
emit mousePressOnOverlay(overlayID, event);
}
void Overlays::sendMouseReleaseOnOverlay(unsigned int overlayID, const PointerEvent& event) {
emit mouseReleaseOnOverlay(overlayID, event);
}
void Overlays::sendMouseMoveOnOverlay(unsigned int overlayID, const PointerEvent& event) {
emit mouseMoveOnOverlay(overlayID, event);
}
void Overlays::sendHoverEnterOverlay(unsigned int id, PointerEvent event) {
emit hoverEnterOverlay(id, event);
}
void Overlays::sendHoverOverOverlay(unsigned int id, PointerEvent event) {
emit hoverOverOverlay(id, event);
}
void Overlays::sendHoverLeaveOverlay(unsigned int id, PointerEvent event) {
emit hoverLeaveOverlay(id, event);
}
unsigned int Overlays::getKeyboardFocusOverlay() const {
return qApp->getKeyboardFocusOverlay();
}
void Overlays::setKeyboardFocusOverlay(unsigned int id) {
qApp->setKeyboardFocusOverlay(id);
}
float Overlays::width() const {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
return offscreenUi->getWindow()->size().width();
@ -607,3 +647,152 @@ float Overlays::height() const {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
return offscreenUi->getWindow()->size().height();
}
static const uint32_t MOUSE_POINTER_ID = 0;
static glm::vec2 projectOntoOverlayXYPlane(glm::vec3 position, glm::quat rotation, glm::vec2 dimensions, const PickRay& pickRay,
const RayToOverlayIntersectionResult& rayPickResult) {
// Project the intersection point onto the local xy plane of the overlay.
float distance;
glm::vec3 planePosition = position;
glm::vec3 planeNormal = rotation * Vectors::UNIT_Z;
glm::vec3 overlayDimensions = glm::vec3(dimensions.x, dimensions.y, 0.0f);
glm::vec3 rayDirection = pickRay.direction;
glm::vec3 rayStart = pickRay.origin;
glm::vec3 p;
if (rayPlaneIntersection(planePosition, planeNormal, rayStart, rayDirection, distance)) {
p = rayStart + rayDirection * distance;
} else {
p = rayPickResult.intersection;
}
glm::vec3 localP = glm::inverse(rotation) * (p - position);
glm::vec3 normalizedP = (localP / overlayDimensions) + glm::vec3(0.5f);
return glm::vec2(normalizedP.x * overlayDimensions.x,
(1.0f - normalizedP.y) * overlayDimensions.y); // flip y-axis
}
static uint32_t toPointerButtons(const QMouseEvent& event) {
uint32_t buttons = 0;
buttons |= event.buttons().testFlag(Qt::LeftButton) ? PointerEvent::PrimaryButton : 0;
buttons |= event.buttons().testFlag(Qt::RightButton) ? PointerEvent::SecondaryButton : 0;
buttons |= event.buttons().testFlag(Qt::MiddleButton) ? PointerEvent::TertiaryButton : 0;
return buttons;
}
static PointerEvent::Button toPointerButton(const QMouseEvent& event) {
switch (event.button()) {
case Qt::LeftButton:
return PointerEvent::PrimaryButton;
case Qt::RightButton:
return PointerEvent::SecondaryButton;
case Qt::MiddleButton:
return PointerEvent::TertiaryButton;
default:
return PointerEvent::NoButtons;
}
}
PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay ray,
RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType) {
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(overlay);
QReadLocker lock(&_lock);
auto position = thisOverlay->getPosition();
auto rotation = thisOverlay->getRotation();
auto dimensions = thisOverlay->getSize();
glm::vec2 pos2D = projectOntoOverlayXYPlane(position, rotation, dimensions, ray, rayPickResult);
PointerEvent pointerEvent(eventType, MOUSE_POINTER_ID,
pos2D, rayPickResult.intersection,
rayPickResult.surfaceNormal, ray.direction,
toPointerButton(*event), toPointerButtons(*event));
return pointerEvent;
}
void Overlays::mousePressEvent(QMouseEvent* event) {
PerformanceTimer perfTimer("Overlays::mousePressEvent");
PickRay ray = qApp->computePickRay(event->x(), event->y());
RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray);
if (rayPickResult.intersects) {
_currentClickingOnOverlayID = rayPickResult.overlayID;
// Only Web overlays can have focus.
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(_currentClickingOnOverlayID));
if (thisOverlay) {
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Press);
emit mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent);
} else {
emit mousePressOffOverlay();
}
} else {
emit mousePressOffOverlay();
}
}
void Overlays::mouseReleaseEvent(QMouseEvent* event) {
PerformanceTimer perfTimer("Overlays::mouseReleaseEvent");
PickRay ray = qApp->computePickRay(event->x(), event->y());
RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray);
if (rayPickResult.intersects) {
// Only Web overlays can have focus.
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(rayPickResult.overlayID));
if (thisOverlay) {
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Release);
emit mouseReleaseOnOverlay(rayPickResult.overlayID, pointerEvent);
}
}
_currentClickingOnOverlayID = UNKNOWN_OVERLAY_ID;
}
void Overlays::mouseMoveEvent(QMouseEvent* event) {
PerformanceTimer perfTimer("Overlays::mouseMoveEvent");
PickRay ray = qApp->computePickRay(event->x(), event->y());
RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray);
if (rayPickResult.intersects) {
// Only Web overlays can have focus.
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(rayPickResult.overlayID));
if (thisOverlay) {
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move);
emit mouseMoveOnOverlay(rayPickResult.overlayID, pointerEvent);
// If previously hovering over a different overlay then leave hover on that overlay.
if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) {
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(_currentHoverOverOverlayID));
if (thisOverlay) {
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move);
emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent);
}
}
// If hovering over a new overlay then enter hover on that overlay.
if (rayPickResult.overlayID != _currentHoverOverOverlayID) {
emit hoverEnterOverlay(rayPickResult.overlayID, pointerEvent);
}
// Hover over current overlay.
emit hoverOverOverlay(rayPickResult.overlayID, pointerEvent);
_currentHoverOverOverlayID = rayPickResult.overlayID;
}
} else {
// If previously hovering an overlay then leave hover.
if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID) {
auto thisOverlay = std::dynamic_pointer_cast<Web3DOverlay>(getOverlay(_currentHoverOverOverlayID));
if (thisOverlay) {
auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move);
emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent);
}
_currentHoverOverOverlayID = UNKNOWN_OVERLAY_ID;
}
}
}

View file

@ -18,11 +18,13 @@
#ifndef hifi_Overlays_h
#define hifi_Overlays_h
#include <QMouseEvent>
#include <QReadWriteLock>
#include <QScriptValue>
#include "Overlay.h"
#include <PointerEvent.h>
#include "Overlay.h"
#include "OverlayPanel.h"
#include "PanelAttachable.h"
@ -51,7 +53,7 @@ class RayToOverlayIntersectionResult {
public:
RayToOverlayIntersectionResult();
bool intersects;
int overlayID;
unsigned int overlayID;
float distance;
BoxFace face;
glm::vec3 surfaceNormal;
@ -75,9 +77,13 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, R
* @namespace Overlays
*/
const unsigned int UNKNOWN_OVERLAY_ID = 0;
class Overlays : public QObject {
Q_OBJECT
Q_PROPERTY(unsigned int keyboardFocusOverlay READ getKeyboardFocusOverlay WRITE setKeyboardFocusOverlay)
public:
Overlays();
@ -94,6 +100,10 @@ public:
unsigned int addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); }
unsigned int addOverlay(Overlay::Pointer overlay);
void mousePressEvent(QMouseEvent* event);
void mouseReleaseEvent(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event);
void cleanupAllOverlays();
public slots:
@ -147,6 +157,15 @@ public slots:
*/
QString getOverlayType(unsigned int overlayId) const;
/**jsdoc
* Get the overlay Script object.
*
* @function Overlays.getOverlayObject
* @param {Overlays.OverlayID} overlayID The ID of the overlay to get the script object of.
* @return {Object} The script object for the overlay if found.
*/
QObject* getOverlayObject(unsigned int id);
/**jsdoc
* Get the ID of the overlay at a particular point on the HUD/screen.
*
@ -239,6 +258,17 @@ public slots:
/// return true if there is a panel with that id else false
bool isAddedPanel(unsigned int id) { return _panels.contains(id); }
void sendMousePressOnOverlay(unsigned int overlayID, const PointerEvent& event);
void sendMouseReleaseOnOverlay(unsigned int overlayID, const PointerEvent& event);
void sendMouseMoveOnOverlay(unsigned int overlayID, const PointerEvent& event);
void sendHoverEnterOverlay(unsigned int id, PointerEvent event);
void sendHoverOverOverlay(unsigned int id, PointerEvent event);
void sendHoverLeaveOverlay(unsigned int id, PointerEvent event);
unsigned int getKeyboardFocusOverlay() const;
void setKeyboardFocusOverlay(unsigned int id);
signals:
/**jsdoc
* Emitted when an overlay is deleted
@ -249,6 +279,15 @@ signals:
void overlayDeleted(unsigned int id);
void panelDeleted(unsigned int id);
void mousePressOnOverlay(unsigned int overlayID, const PointerEvent& event);
void mouseReleaseOnOverlay(unsigned int overlayID, const PointerEvent& event);
void mouseMoveOnOverlay(unsigned int overlayID, const PointerEvent& event);
void mousePressOffOverlay();
void hoverEnterOverlay(unsigned int overlayID, const PointerEvent& event);
void hoverOverOverlay(unsigned int overlayID, const PointerEvent& event);
void hoverLeaveOverlay(unsigned int overlayID, const PointerEvent& event);
private:
void cleanupOverlaysToDelete();
@ -262,8 +301,12 @@ private:
QReadWriteLock _deleteLock;
QScriptEngine* _scriptEngine;
bool _enabled = true;
PointerEvent calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, RayToOverlayIntersectionResult rayPickResult,
QMouseEvent* event, PointerEvent::EventType eventType);
unsigned int _currentClickingOnOverlayID { UNKNOWN_OVERLAY_ID };
unsigned int _currentHoverOverOverlayID { UNKNOWN_OVERLAY_ID };
};
#endif // hifi_Overlays_h

View file

@ -1,7 +1,6 @@
//
// Web3DOverlay.cpp
//
//
// Created by Clement on 7/1/14.
// Modified and renamed by Zander Otavka on 8/4/15
// Copyright 2014 High Fidelity, Inc.
@ -12,8 +11,12 @@
#include "Web3DOverlay.h"
#include <Application.h>
#include <QQuickWindow>
#include <QtGui/QOpenGLContext>
#include <QtQuick/QQuickItem>
#include <QtQml/QQmlContext>
#include <DependencyManager.h>
#include <GeometryCache.h>
@ -28,17 +31,24 @@
static const float DPI = 30.47f;
static const float INCHES_TO_METERS = 1.0f / 39.3701f;
static float OPAQUE_ALPHA_THRESHOLD = 0.99f;
static const float METERS_TO_INCHES = 39.3701f;
static const float OPAQUE_ALPHA_THRESHOLD = 0.99f;
QString const Web3DOverlay::TYPE = "web3d";
Web3DOverlay::Web3DOverlay() : _dpi(DPI) {
_touchDevice.setCapabilities(QTouchDevice::Position);
_touchDevice.setType(QTouchDevice::TouchScreen);
_touchDevice.setName("RenderableWebEntityItemTouchDevice");
_touchDevice.setMaximumTouchPoints(4);
_geometryId = DependencyManager::get<GeometryCache>()->allocateID();
}
Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) :
Billboard3DOverlay(Web3DOverlay),
_url(Web3DOverlay->_url),
_scriptURL(Web3DOverlay->_scriptURL),
_dpi(Web3DOverlay->_dpi),
_resolution(Web3DOverlay->_resolution)
{
@ -50,6 +60,19 @@ Web3DOverlay::~Web3DOverlay() {
_webSurface->pause();
_webSurface->disconnect(_connection);
QObject::disconnect(_mousePressConnection);
_mousePressConnection = QMetaObject::Connection();
QObject::disconnect(_mouseReleaseConnection);
_mouseReleaseConnection = QMetaObject::Connection();
QObject::disconnect(_mouseMoveConnection);
_mouseMoveConnection = QMetaObject::Connection();
QObject::disconnect(_hoverLeaveConnection);
_hoverLeaveConnection = QMetaObject::Connection();
QObject::disconnect(_emitScriptEventConnection);
_emitScriptEventConnection = QMetaObject::Connection();
QObject::disconnect(_webEventReceivedConnection);
_webEventReceivedConnection = QMetaObject::Connection();
// The lifetime of the QML surface MUST be managed by the main thread
// Additionally, we MUST use local variables copied by value, rather than
@ -67,11 +90,15 @@ Web3DOverlay::~Web3DOverlay() {
}
void Web3DOverlay::update(float deltatime) {
// FIXME: applyTransformTo causes tablet overlay to detach from tablet entity.
// Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true?
/*
if (usecTimestampNow() > _transformExpiry) {
Transform transform = getTransform();
applyTransformTo(transform);
setTransform(transform);
}
*/
}
void Web3DOverlay::render(RenderArgs* args) {
@ -92,21 +119,62 @@ void Web3DOverlay::render(RenderArgs* args) {
// and the current rendering load)
_webSurface->setMaxFps(10);
_webSurface->create(currentContext);
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/"));
_webSurface->load("WebView.qml");
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/"));
_webSurface->load("Web3DOverlay.qml");
_webSurface->resume();
_webSurface->getRootItem()->setProperty("url", _url);
_webSurface->getRootItem()->setProperty("scriptURL", _scriptURL);
_webSurface->getRootContext()->setContextProperty("ApplicationInterface", qApp);
_webSurface->resize(QSize(_resolution.x, _resolution.y));
currentContext->makeCurrent(currentSurface);
auto forwardPointerEvent = [=](unsigned int overlayID, const PointerEvent& event) {
if (overlayID == getOverlayID()) {
handlePointerEvent(event);
}
};
_mousePressConnection = connect(&(qApp->getOverlays()), &Overlays::mousePressOnOverlay, forwardPointerEvent);
_mouseReleaseConnection = connect(&(qApp->getOverlays()), &Overlays::mouseReleaseOnOverlay, forwardPointerEvent);
_mouseMoveConnection = connect(&(qApp->getOverlays()), &Overlays::mouseMoveOnOverlay, forwardPointerEvent);
_hoverLeaveConnection = connect(&(qApp->getOverlays()), &Overlays::hoverLeaveOverlay,
[=](unsigned int overlayID, const PointerEvent& event) {
if (this->_pressed && this->getOverlayID() == overlayID) {
// If the user mouses off the overlay while the button is down, simulate a touch end.
QTouchEvent::TouchPoint point;
point.setId(event.getID());
point.setState(Qt::TouchPointReleased);
glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi);
QPointF windowPoint(windowPos.x, windowPos.y);
point.setScenePos(windowPoint);
point.setPos(windowPoint);
QList<QTouchEvent::TouchPoint> touchPoints;
touchPoints.push_back(point);
QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased,
touchPoints);
touchEvent->setWindow(_webSurface->getWindow());
touchEvent->setDevice(&_touchDevice);
touchEvent->setTarget(_webSurface->getRootItem());
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent);
}
});
_emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent);
_webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived);
}
vec2 size = _resolution / _dpi * INCHES_TO_METERS;
vec2 halfSize = size / 2.0f;
vec2 halfSize = getSize() / 2.0f;
vec4 color(toGlm(getColor()), getAlpha());
Transform transform = getTransform();
// FIXME: applyTransformTo causes tablet overlay to detach from tablet entity.
// Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true?
/*
applyTransformTo(transform, true);
setTransform(transform);
*/
if (glm::length2(getDimensions()) != 1.0f) {
transform.postScale(vec3(getDimensions(), 1.0f));
}
@ -144,6 +212,78 @@ const render::ShapeKey Web3DOverlay::getShapeKey() {
return builder.build();
}
QObject* Web3DOverlay::getEventHandler() {
if (!_webSurface) {
return nullptr;
}
return _webSurface->getEventHandler();
}
void Web3DOverlay::setProxyWindow(QWindow* proxyWindow) {
if (!_webSurface) {
return;
}
_webSurface->setProxyWindow(proxyWindow);
}
void Web3DOverlay::handlePointerEvent(const PointerEvent& event) {
if (!_webSurface) {
return;
}
glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi);
QPointF windowPoint(windowPos.x, windowPos.y);
if (event.getType() == PointerEvent::Move) {
// Forward a mouse move event to the Web surface.
QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton,
Qt::NoButton, Qt::NoModifier);
QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent);
}
if (event.getType() == PointerEvent::Press) {
this->_pressed = true;
} else if (event.getType() == PointerEvent::Release) {
this->_pressed = false;
}
QEvent::Type type;
Qt::TouchPointState touchPointState;
switch (event.getType()) {
case PointerEvent::Press:
type = QEvent::TouchBegin;
touchPointState = Qt::TouchPointPressed;
break;
case PointerEvent::Release:
type = QEvent::TouchEnd;
touchPointState = Qt::TouchPointReleased;
break;
case PointerEvent::Move:
default:
type = QEvent::TouchUpdate;
touchPointState = Qt::TouchPointMoved;
break;
}
QTouchEvent::TouchPoint point;
point.setId(event.getID());
point.setState(touchPointState);
point.setPos(windowPoint);
point.setScreenPos(windowPoint);
QList<QTouchEvent::TouchPoint> touchPoints;
touchPoints.push_back(point);
QTouchEvent* touchEvent = new QTouchEvent(type);
touchEvent->setWindow(_webSurface->getWindow());
touchEvent->setDevice(&_touchDevice);
touchEvent->setTarget(_webSurface->getRootItem());
touchEvent->setTouchPoints(touchPoints);
touchEvent->setTouchPointStates(touchPointState);
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent);
}
void Web3DOverlay::setProperties(const QVariantMap& properties) {
Billboard3DOverlay::setProperties(properties);
@ -155,6 +295,14 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) {
}
}
auto scriptURLValue = properties["scriptURL"];
if (scriptURLValue.isValid()) {
QString newScriptURL = scriptURLValue.toString();
if (newScriptURL != _scriptURL) {
setScriptURL(newScriptURL);
}
}
auto resolution = properties["resolution"];
if (resolution.isValid()) {
bool valid;
@ -164,7 +312,6 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) {
}
}
auto dpi = properties["dpi"];
if (dpi.isValid()) {
_dpi = dpi.toFloat();
@ -175,6 +322,12 @@ QVariant Web3DOverlay::getProperty(const QString& property) {
if (property == "url") {
return _url;
}
if (property == "scriptURL") {
return _scriptURL;
}
if (property == "resolution") {
return vec2toVariant(_resolution);
}
if (property == "dpi") {
return _dpi;
}
@ -188,22 +341,38 @@ void Web3DOverlay::setURL(const QString& url) {
_webSurface->getRootItem()->setProperty("url", url);
});
}
}
void Web3DOverlay::setScriptURL(const QString& scriptURL) {
_scriptURL = scriptURL;
if (_webSurface) {
AbstractViewStateInterface::instance()->postLambdaEvent([this, scriptURL] {
_webSurface->getRootItem()->setProperty("scriptURL", scriptURL);
});
}
}
glm::vec2 Web3DOverlay::getSize() {
return _resolution / _dpi * INCHES_TO_METERS * getDimensions();
};
bool Web3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) {
// FIXME - face and surfaceNormal not being returned
// Make sure position and rotation is updated.
Transform transform;
applyTransformTo(transform, true);
setTransform(transform);
// Don't call applyTransformTo() or setTransform() here because this code runs too frequently.
vec2 size = _resolution / _dpi * INCHES_TO_METERS * vec2(getDimensions());
// Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale.
return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), size, distance);
return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), getSize(), distance);
}
Web3DOverlay* Web3DOverlay::createClone() const {
return new Web3DOverlay(this);
}
void Web3DOverlay::emitScriptEvent(const QVariant& message) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message));
} else {
emit scriptEventReceived(message);
}
}

View file

@ -11,6 +11,10 @@
#include "Billboard3DOverlay.h"
#include <QTouchEvent>
#include <PointerEvent.h>
class OffscreenQmlSurface;
class Web3DOverlay : public Billboard3DOverlay {
@ -29,25 +33,51 @@ public:
virtual void update(float deltatime) override;
QObject* getEventHandler();
void setProxyWindow(QWindow* proxyWindow);
void handlePointerEvent(const PointerEvent& event);
// setters
void setURL(const QString& url);
void setScriptURL(const QString& script);
void setProperties(const QVariantMap& properties) override;
QVariant getProperty(const QString& property) override;
glm::vec2 getSize();
virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance,
BoxFace& face, glm::vec3& surfaceNormal) override;
virtual Web3DOverlay* createClone() const override;
public slots:
void emitScriptEvent(const QVariant& scriptMessage);
signals:
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
private:
QSharedPointer<OffscreenQmlSurface> _webSurface;
QMetaObject::Connection _connection;
gpu::TexturePointer _texture;
QString _url;
QString _scriptURL;
float _dpi;
vec2 _resolution{ 640, 480 };
int _geometryId { 0 };
bool _pressed{ false };
QTouchDevice _touchDevice;
QMetaObject::Connection _mousePressConnection;
QMetaObject::Connection _mouseReleaseConnection;
QMetaObject::Connection _mouseMoveConnection;
QMetaObject::Connection _hoverLeaveConnection;
QMetaObject::Connection _emitScriptEventConnection;
QMetaObject::Connection _webEventReceivedConnection;
};
#endif // hifi_Web3DOverlay_h

View file

@ -836,6 +836,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
// If the new hover entity does not match the previous hover entity then we are entering the new one
// this is true if the _currentHoverOverEntityID is known or unknown
if (rayPickResult.entityID != _currentHoverOverEntityID) {
emit hoverEnterEntity(rayPickResult.entityID, pointerEvent);
if (_entitiesScriptEngine) {
_entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "hoverEnterEntity", pointerEvent);
}

View file

@ -533,7 +533,8 @@ void RenderableModelEntityItem::update(const quint64& now) {
properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it
auto extents = _model->getMeshExtents();
properties.setDimensions(extents.maximum - extents.minimum);
qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL());
qCDebug(entitiesrenderer) << "Autoresizing" << (!getName().isEmpty() ? getName() : getModelURL())
<< "from mesh extents";
QMetaObject::invokeMethod(DependencyManager::get<EntityScriptingInterface>().data(), "editEntity",
Qt::QueuedConnection,
Q_ARG(QUuid, getEntityItemID()),

View file

@ -326,8 +326,6 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) {
touchEvent->setTouchPoints(touchPoints);
touchEvent->setTouchPointStates(touchPointState);
_lastTouchEvent = *touchEvent;
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent);
}
}

View file

@ -59,9 +59,7 @@ private:
QSharedPointer<OffscreenQmlSurface> _webSurface;
QMetaObject::Connection _connection;
gpu::TexturePointer _texture;
ivec2 _lastPress { INT_MIN };
bool _pressed{ false };
QTouchEvent _lastTouchEvent { QEvent::TouchUpdate };
uint64_t _lastRenderTime{ 0 };
QTouchDevice _touchDevice;

View file

@ -316,7 +316,7 @@ private:
float _localRenderAlpha;
bool _localRenderAlphaChanged;
bool _defaultSettings;
bool _dimensionsInitialized = true; // Only false if creating an entity localy with no dimensions properties
bool _dimensionsInitialized = true; // Only false if creating an entity locally with no dimensions properties
// NOTE: The following are pseudo client only properties. They are only used in clients which can access
// properties of model geometry. But these properties are not serialized like other properties.

View file

@ -35,6 +35,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged);
connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged);
connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged);
connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged);
}
void EntityScriptingInterface::queueEntityMessage(PacketType packetType,
@ -63,6 +64,11 @@ bool EntityScriptingInterface::canRezTmp() {
return nodeList->getThisNodeCanRezTmp();
}
bool EntityScriptingInterface::canWriteAssets() {
auto nodeList = DependencyManager::get<NodeList>();
return nodeList->getThisNodeCanWriteAssets();
}
void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
if (_entityTree) {
disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity);

View file

@ -110,6 +110,12 @@ public slots:
*/
Q_INVOKABLE bool canRezTmp();
/**jsdoc
* @function Entities.canWriteAsseets
* @return {bool} `true` if the DomainServer will allow this Node/Avatar to write to the asset server
*/
Q_INVOKABLE bool canWriteAssets();
/**jsdoc
* Add a new entity with the specified properties. If `clientOnly` is true, the entity will
* not be sent to the server and will only be visible/accessible on the local client.
@ -282,6 +288,7 @@ signals:
void canAdjustLocksChanged(bool canAdjustLocks);
void canRezChanged(bool canRez);
void canRezTmpChanged(bool canRez);
void canWriteAssetsChanged(bool canWriteAssets);
void mousePressOnEntity(const EntityItemID& entityItemID, const PointerEvent& event);
void mouseMoveOnEntity(const EntityItemID& entityItemID, const PointerEvent& event);

View file

@ -26,3 +26,16 @@ void ResourceRequest::send() {
_state = InProgress;
doSend();
}
QString ResourceRequest::getResultString() const {
switch (_result) {
case Success: return "Success";
case Error: return "Error";
case Timeout: return "Timeout";
case ServerUnavailable: return "Server Unavailable";
case AccessDenied: return "Access Denied";
case InvalidURL: return "Invalid URL";
case NotFound: return "Not Found";
default: return "Unspecified Error";
}
}

View file

@ -42,6 +42,7 @@ public:
QByteArray getData() { return _data; }
State getState() const { return _state; }
Result getResult() const { return _result; }
QString getResultString() const;
QUrl getUrl() const { return _url; }
bool loadedFromCache() const { return _loadedFromCache; }

View file

@ -304,6 +304,12 @@ template <> const Item::Bound payloadGetBound(const ModelMeshPartPayload::Pointe
}
return Item::Bound();
}
template <> int payloadGetLayer(const ModelMeshPartPayload::Pointer& payload) {
if (payload) {
return payload->getLayer();
}
return 0;
}
template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload) {
if (payload) {
@ -378,6 +384,10 @@ ItemKey ModelMeshPartPayload::getKey() const {
builder.withInvisible();
}
if (_model->isLayeredInFront()) {
builder.withLayered();
}
if (_isBlendShaped || _isSkinned) {
builder.withDeformed();
}
@ -396,6 +406,17 @@ ItemKey ModelMeshPartPayload::getKey() const {
return builder.build();
}
int ModelMeshPartPayload::getLayer() const {
// MAgic number while we are defining the layering mechanism:
const int LAYER_3D_FRONT = 1;
const int LAYER_3D = 0;
if (_model->isLayeredInFront()) {
return LAYER_3D_FRONT;
} else {
return LAYER_3D;
}
}
ShapeKey ModelMeshPartPayload::getShapeKey() const {
// guard against partially loaded meshes

View file

@ -95,6 +95,7 @@ public:
// Render Item interface
render::ItemKey getKey() const override;
int getLayer() const;
render::ShapeKey getShapeKey() const override; // shape interface
void render(RenderArgs* args) const override;
@ -122,6 +123,7 @@ private:
namespace render {
template <> const ItemKey payloadGetKey(const ModelMeshPartPayload::Pointer& payload);
template <> const Item::Bound payloadGetBound(const ModelMeshPartPayload::Pointer& payload);
template <> int payloadGetLayer(const ModelMeshPartPayload::Pointer& payload);
template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload);
template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, RenderArgs* args);
}

View file

@ -620,6 +620,22 @@ void Model::setVisibleInScene(bool newValue, std::shared_ptr<render::Scene> scen
}
}
void Model::setLayeredInFront(bool layered, std::shared_ptr<render::Scene> scene) {
if (_isLayeredInFront != layered) {
_isLayeredInFront = layered;
render::PendingChanges pendingChanges;
foreach(auto item, _modelMeshRenderItems.keys()) {
pendingChanges.resetItem(item, _modelMeshRenderItems[item]);
}
foreach(auto item, _collisionRenderItems.keys()) {
pendingChanges.resetItem(item, _collisionRenderItems[item]);
}
scene->enqueuePendingChanges(pendingChanges);
}
}
bool Model::addToScene(std::shared_ptr<render::Scene> scene,
render::PendingChanges& pendingChanges,
render::Item::Status::Getters& statusGetters) {

View file

@ -80,6 +80,7 @@ public:
// new Scene/Engine rendering support
void setVisibleInScene(bool newValue, std::shared_ptr<render::Scene> scene);
void setLayeredInFront(bool layered, std::shared_ptr<render::Scene> scene);
bool needsFixupInScene() const;
bool needsReload() const { return _needsReload; }
@ -98,6 +99,8 @@ public:
bool isVisible() const { return _isVisible; }
bool isLayeredInFront() const { return _isLayeredInFront; }
void updateRenderItems();
void setRenderItemsNeedUpdate() { _renderItemsNeedUpdate = true; }
bool getRenderItemsNeedUpdate() { return _renderItemsNeedUpdate; }
@ -413,6 +416,8 @@ protected:
int _renderInfoDrawCalls { 0 };
int _renderInfoHasTransparent { false };
bool _isLayeredInFront { false };
private:
float _loadingPriority { 0.0f };

View file

@ -32,7 +32,7 @@ FileScriptingInterface::FileScriptingInterface(QObject* parent) : QObject(parent
// nothing for now
}
void FileScriptingInterface::runUnzip(QString path, QUrl url) {
void FileScriptingInterface::runUnzip(QString path, QUrl url, bool autoAdd) {
qCDebug(scriptengine) << "Url that was downloaded: " + url.toString();
qCDebug(scriptengine) << "Path where download is saved: " + path;
QString fileName = "/" + path.section("/", -1);
@ -45,15 +45,13 @@ void FileScriptingInterface::runUnzip(QString path, QUrl url) {
}
QString file = unzipFile(path, tempDir);
QString filename = QUrl::fromLocalFile(file).toString();
if (file != "") {
qCDebug(scriptengine) << "Object file to upload: " + file;
QUrl url = QUrl::fromLocalFile(file);
emit unzipSuccess(url.toString());
qCDebug(scriptengine) << "File to upload: " + filename;
} else {
qCDebug(scriptengine) << "unzip failed";
qCDebug(scriptengine) << "Unzip failed";
}
qCDebug(scriptengine) << "Removing temporary directory at: " + tempDir;
QDir(tempDir).removeRecursively();
emit unzipResult(path, filename, autoAdd);
}
// fix to check that we are only referring to a temporary directory
@ -69,21 +67,6 @@ bool FileScriptingInterface::isTempDir(QString tempDir) {
return (testContainer == tempContainer);
}
// checks whether the webview is displaying a Clara.io page for Marketplaces.qml
bool FileScriptingInterface::isClaraLink(QUrl url) {
return (url.toString().contains("clara.io") && !url.toString().contains("clara.io/signup"));
}
bool FileScriptingInterface::isZippedFbx(QUrl url) {
return (url.toString().endsWith("fbx.zip"));
}
// checks whether a user tries to download a file that is not in .fbx format
bool FileScriptingInterface::isZipped(QUrl url) {
return (url.toString().endsWith(".zip"));
}
// this function is not in use
QString FileScriptingInterface::getTempDir() {
QTemporaryDir dir;
dir.setAutoRemove(false);
@ -109,7 +92,6 @@ void FileScriptingInterface::downloadZip(QString path, const QString link) {
request->send();
}
QString FileScriptingInterface::unzipFile(QString path, QString tempDir) {
QDir dir(path);

View file

@ -22,17 +22,13 @@ class FileScriptingInterface : public QObject {
public:
FileScriptingInterface(QObject* parent);
public slots:
bool isZippedFbx(QUrl url);
bool isZipped(QUrl url);
bool isClaraLink(QUrl url);
QString convertUrlToPath(QUrl url);
void runUnzip(QString path, QUrl url);
void runUnzip(QString path, QUrl url, bool autoAdd);
QString getTempDir();
signals:
void unzipSuccess(QString url);
void unzipResult(QString zipFile, QString unzipFile, bool autoAdd);
private:
bool isTempDir(QString tempDir);

View file

@ -16,6 +16,7 @@
#include "OffscreenUi.h"
static const char* const URL_PROPERTY = "source";
static const char* const SCRIPT_PROPERTY = "scriptUrl";
// Method called by Qt scripts to create a new web window in the overlay
QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
@ -31,51 +32,6 @@ QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngi
return engine->newQObject(retVal);
}
void QmlWebWindowClass::emitScriptEvent(const QVariant& scriptMessage) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage));
} else {
emit scriptEventReceived(scriptMessage);
}
}
void QmlWebWindowClass::emitWebEvent(const QVariant& webMessage) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage));
} else {
// Special case to handle raising and lowering the virtual keyboard.
const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD";
const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC";
const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD";
QString messageString = webMessage.type() == QVariant::String ? webMessage.toString() : "";
if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) {
setKeyboardRaised(asQuickItem(), true, messageString == RAISE_KEYBOARD_NUMERIC);
} else if (messageString == LOWER_KEYBOARD) {
setKeyboardRaised(asQuickItem(), false);
} else {
emit webEventReceived(webMessage);
}
}
}
void QmlWebWindowClass::setKeyboardRaised(QObject* object, bool raised, bool numeric) {
if (!object) {
return;
}
QQuickItem* item = dynamic_cast<QQuickItem*>(object);
while (item) {
if (item->property("keyboardRaised").isValid()) {
if (item->property("punctuationMode").isValid()) {
item->setProperty("punctuationMode", QVariant(numeric));
}
item->setProperty("keyboardRaised", QVariant(raised));
return;
}
item = dynamic_cast<QQuickItem*>(item->parentItem());
}
}
QString QmlWebWindowClass::getURL() const {
QVariant result = DependencyManager::get<OffscreenUi>()->returnFromUiThread([&]()->QVariant {
if (_qmlWindow.isNull()) {
@ -93,3 +49,11 @@ void QmlWebWindowClass::setURL(const QString& urlString) {
}
});
}
void QmlWebWindowClass::setScriptURL(const QString& script) {
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
if (!_qmlWindow.isNull()) {
_qmlWindow->setProperty(SCRIPT_PROPERTY, script);
}
});
}

View file

@ -22,20 +22,13 @@ public:
public slots:
QString getURL() const;
void setURL(const QString& url);
void emitScriptEvent(const QVariant& scriptMessage);
void emitWebEvent(const QVariant& webMessage);
void setScriptURL(const QString& script);
signals:
void urlChanged();
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
protected:
QString qmlSource() const override { return "QmlWebWindow.qml"; }
private:
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
};
#endif

View file

@ -150,6 +150,52 @@ void QmlWindowClass::sendToQml(const QVariant& message) {
QMetaObject::invokeMethod(asQuickItem(), "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message));
}
void QmlWindowClass::emitScriptEvent(const QVariant& scriptMessage) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage));
} else {
emit scriptEventReceived(scriptMessage);
}
}
void QmlWindowClass::emitWebEvent(const QVariant& webMessage) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage));
} else {
// Special case to handle raising and lowering the virtual keyboard.
const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD";
const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC";
const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD";
QString messageString = webMessage.type() == QVariant::String ? webMessage.toString() : "";
if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) {
setKeyboardRaised(asQuickItem(), true, messageString == RAISE_KEYBOARD_NUMERIC);
} else if (messageString == LOWER_KEYBOARD) {
setKeyboardRaised(asQuickItem(), false);
} else {
emit webEventReceived(webMessage);
}
}
}
void QmlWindowClass::setKeyboardRaised(QObject* object, bool raised, bool numeric) {
if (!object) {
return;
}
QQuickItem* item = dynamic_cast<QQuickItem*>(object);
while (item) {
if (item->property("keyboardRaised").isValid()) {
if (item->property("punctuationMode").isValid()) {
item->setProperty("punctuationMode", QVariant(numeric));
}
item->setProperty("keyboardRaised", QVariant(raised));
return;
}
item = dynamic_cast<QQuickItem*>(item->parentItem());
}
}
QmlWindowClass::~QmlWindowClass() {
close();
}

View file

@ -51,6 +51,10 @@ public slots:
// Scripts can use this to send a message to the QML object
void sendToQml(const QVariant& message);
// QmlWindow content may include WebView requiring EventBridge.
void emitScriptEvent(const QVariant& scriptMessage);
void emitWebEvent(const QVariant& webMessage);
signals:
void visibleChanged();
void positionChanged();
@ -61,6 +65,10 @@ signals:
// Scripts can connect to this signal to receive messages from the QML object
void fromQml(const QVariant& message);
// QmlWindow content may include WebView requiring EventBridge.
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
protected slots:
void hasMoved(QVector2D);
void hasClosed();
@ -81,6 +89,10 @@ protected:
bool _toolWindow { false };
QPointer<QObject> _qmlWindow;
QString _source;
private:
// QmlWindow content may include WebView requiring EventBridge.
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
};
#endif

View file

@ -140,6 +140,10 @@ var ONE_VEC = {
var NULL_UUID = "{00000000-0000-0000-0000-000000000000}";
var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 };
var INCHES_TO_METERS = 1.0 / 39.3701;
// these control how long an abandoned pointer line or action will hang around
var ACTION_TTL = 15; // seconds
var ACTION_TTL_REFRESH = 5;
@ -186,6 +190,7 @@ var STATE_NEAR_TRIGGER = 4;
var STATE_FAR_TRIGGER = 5;
var STATE_HOLD = 6;
var STATE_ENTITY_TOUCHING = 7;
var STATE_OVERLAY_TOUCHING = 8;
var holdEnabled = true;
var nearGrabEnabled = true;
@ -208,6 +213,8 @@ var mostRecentSearchingHand = RIGHT_HAND;
var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx";
var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse.
CONTROLLER_STATE_MACHINE[STATE_OFF] = {
name: "off",
enterMethod: "offEnter",
@ -249,6 +256,12 @@ CONTROLLER_STATE_MACHINE[STATE_ENTITY_TOUCHING] = {
exitMethod: "entityTouchingExit",
updateMethod: "entityTouching"
};
CONTROLLER_STATE_MACHINE[STATE_OVERLAY_TOUCHING] = {
name: "overlayTouching",
enterMethod: "overlayTouchingEnter",
exitMethod: "overlayTouchingExit",
updateMethod: "overlayTouching"
};
function distanceBetweenPointAndEntityBoundingBox(point, entityProps) {
var entityXform = new Xform(entityProps.rotation, entityProps.position);
@ -273,27 +286,48 @@ function angleBetween(a, b) {
return Math.acos(Vec3.dot(Vec3.normalize(a), Vec3.normalize(b)));
}
function projectOntoEntityXYPlane(entityID, worldPos) {
var props = entityPropertiesCache.getProps(entityID);
var invRot = Quat.inverse(props.rotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, props.position));
var invDimensions = { x: 1 / props.dimensions.x,
y: 1 / props.dimensions.y,
z: 1 / props.dimensions.z };
var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint);
return { x: normalizedPos.x * props.dimensions.x,
y: (1 - normalizedPos.y) * props.dimensions.y }; // flip y-axis
function projectOntoXYPlane(worldPos, position, rotation, dimensions, registrationPoint) {
var invRot = Quat.inverse(rotation);
var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position));
var invDimensions = { x: 1 / dimensions.x,
y: 1 / dimensions.y,
z: 1 / dimensions.z };
var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), registrationPoint);
return { x: normalizedPos.x * dimensions.x,
y: (1 - normalizedPos.y) * dimensions.y }; // flip y-axis
}
function handLaserIntersectEntity(entityID, start) {
function projectOntoEntityXYPlane(entityID, worldPos) {
var props = entityPropertiesCache.getProps(entityID);
return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint);
}
function projectOntoOverlayXYPlane(overlayID, worldPos) {
var position = Overlays.getProperty(overlayID, "position");
var rotation = Overlays.getProperty(overlayID, "rotation");
var dimensions;
var dpi = Overlays.getProperty(overlayID, "dpi");
if (dpi) {
// Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale.
var resolution = Overlays.getProperty(overlayID, "resolution");
resolution.z = 1; // Circumvent divide-by-zero.
var scale = Overlays.getProperty(overlayID, "dimensions");
dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale);
} else {
dimensions = Overlays.getProperty(overlayID, "dimensions");
}
return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT);
}
function handLaserIntersectItem(position, rotation, start) {
var worldHandPosition = start.position;
var worldHandRotation = start.orientation;
var props = entityPropertiesCache.getProps(entityID);
if (props.position) {
var planePosition = props.position;
var planeNormal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1.0});
if (position) {
var planePosition = position;
var planeNormal = Vec3.multiplyQbyV(rotation, {x: 0, y: 0, z: 1.0});
var rayStart = worldHandPosition;
var rayDirection = Quat.getUp(worldHandRotation);
var intersectionInfo = rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection);
@ -318,6 +352,17 @@ function handLaserIntersectEntity(entityID, start) {
}
}
function handLaserIntersectEntity(entityID, start) {
var props = entityPropertiesCache.getProps(entityID);
return handLaserIntersectItem(props.position, props.rotation, start);
}
function handLaserIntersectOverlay(overlayID, start) {
var position = Overlays.getProperty(overlayID, "position");
var rotation = Overlays.getProperty(overlayID, "rotation");
return handLaserIntersectItem(position, rotation, start);
}
function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) {
var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal);
if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) {
@ -727,6 +772,7 @@ function MyController(hand) {
this.actionID = null; // action this script created...
this.grabbedEntity = null; // on this entity.
this.grabbedOverlay = null;
this.state = STATE_OFF;
this.pointer = null; // entity-id of line object
this.entityActivated = false;
@ -1159,6 +1205,7 @@ function MyController(hand) {
var result = {
entityID: null,
overlayID: null,
searchRay: pickRay,
distance: PICK_MAX_DISTANCE
};
@ -1424,6 +1471,7 @@ function MyController(hand) {
var name;
this.grabbedEntity = null;
this.grabbedOverlay = null;
this.isInitialGrab = false;
this.shouldResetParentOnRelease = false;
@ -1517,6 +1565,7 @@ function MyController(hand) {
name = entityPropertiesCache.getProps(entity).name;
if (Entities.keyboardFocusEntity != entity) {
Overlays.keyboardFocusOverlay = 0;
Entities.keyboardFocusEntity = entity;
pointerEvent = {
@ -1536,7 +1585,8 @@ function MyController(hand) {
// send mouse events for button highlights and tooltips.
if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand &&
this.getOtherHandController().state !== STATE_SEARCHING &&
this.getOtherHandController().state !== STATE_ENTITY_TOUCHING)) {
this.getOtherHandController().state !== STATE_ENTITY_TOUCHING &&
this.getOtherHandController().state !== STATE_OVERLAY_TOUCHING)) {
// most recently searching hand has priority over other hand, for the purposes of button highlighting.
pointerEvent = {
@ -1589,6 +1639,65 @@ function MyController(hand) {
}
}
var overlay;
if (rayPickInfo.overlayID) {
overlay = rayPickInfo.overlayID;
if (Overlays.keyboardFocusOverlay != overlay) {
Entities.keyboardFocusEntity = null;
Overlays.keyboardFocusOverlay = overlay;
pointerEvent = {
type: "Move",
id: HARDWARE_MOUSE_ID,
pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection),
pos3D: rayPickInfo.intersection,
normal: rayPickInfo.normal,
direction: rayPickInfo.searchRay.direction,
button: "None"
};
this.hoverOverlay = overlay;
Overlays.sendHoverEnterOverlay(overlay, pointerEvent);
}
// Send mouse events for button highlights and tooltips.
if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand &&
this.getOtherHandController().state !== STATE_SEARCHING &&
this.getOtherHandController().state !== STATE_ENTITY_TOUCHING &&
this.getOtherHandController().state !== STATE_OVERLAY_TOUCHING)) {
// most recently searching hand has priority over other hand, for the purposes of button highlighting.
pointerEvent = {
type: "Move",
id: HARDWARE_MOUSE_ID,
pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection),
pos3D: rayPickInfo.intersection,
normal: rayPickInfo.normal,
direction: rayPickInfo.searchRay.direction,
button: "None"
};
Overlays.sendMouseMoveOnOverlay(overlay, pointerEvent);
Overlays.sendHoverOverOverlay(overlay, pointerEvent);
}
if (this.triggerSmoothedGrab() && !isEditing()) {
this.grabbedOverlay = overlay;
this.setState(STATE_OVERLAY_TOUCHING, "begin touching overlay '" + overlay + "'");
return;
}
} else if (this.hoverOverlay) {
pointerEvent = {
type: "Move",
id: HARDWARE_MOUSE_ID
};
Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent);
this.hoverOverlay = null;
}
this.updateEquipHaptics(potentialEquipHotspot, handPosition);
var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS);
@ -2339,7 +2448,6 @@ function MyController(hand) {
Entities.sendClickReleaseOnEntity(this.grabbedEntity, pointerEvent);
Entities.sendHoverLeaveEntity(this.grabbedEntity, pointerEvent);
}
this.focusedEntity = null;
};
this.entityTouching = function(dt) {
@ -2359,6 +2467,7 @@ function MyController(hand) {
if (intersectInfo) {
if (Entities.keyboardFocusEntity != this.grabbedEntity) {
Overlays.keyboardFocusOverlay = 0;
Entities.keyboardFocusEntity = this.grabbedEntity;
}
@ -2393,6 +2502,108 @@ function MyController(hand) {
}
};
this.overlayTouchingEnter = function () {
// Test for intersection between controller laser and Web overlay plane.
var intersectInfo =
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true));
if (intersectInfo) {
var pointerEvent = {
type: "Press",
id: HARDWARE_MOUSE_ID,
pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point),
pos3D: intersectInfo.point,
normal: intersectInfo.normal,
direction: intersectInfo.searchRay.direction,
button: "Primary",
isPrimaryHeld: true
};
Overlays.sendMousePressOnOverlay(this.grabbedOverlay, pointerEvent);
this.touchingEnterTimer = 0;
this.touchingEnterPointerEvent = pointerEvent;
this.touchingEnterPointerEvent.button = "None";
this.deadspotExpired = false;
}
};
this.overlayTouchingExit = function () {
// Test for intersection between controller laser and Web overlay plane.
var intersectInfo =
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true));
if (intersectInfo) {
var pointerEvent;
if (this.deadspotExpired) {
pointerEvent = {
type: "Release",
id: HARDWARE_MOUSE_ID,
pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point),
pos3D: intersectInfo.point,
normal: intersectInfo.normal,
direction: intersectInfo.searchRay.direction,
button: "Primary"
};
} else {
pointerEvent = this.touchingEnterPointerEvent;
pointerEvent.type = "Release";
pointerEvent.button = "Primary";
pointerEvent.isPrimaryHeld = false;
}
Overlays.sendMouseReleaseOnOverlay(this.grabbedOverlay, pointerEvent);
Overlays.sendHoverLeaveOverlay(this.grabbedOverlay, pointerEvent);
}
};
this.overlayTouching = function (dt) {
this.touchingEnterTimer += dt;
if (!this.triggerSmoothedGrab()) {
this.setState(STATE_OFF, "released trigger");
return;
}
// Test for intersection between controller laser and Web overlay plane.
var intersectInfo =
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true));
if (intersectInfo) {
if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) {
Entities.keyboardFocusEntity = null;
Overlays.keyboardFocusOverlay = this.grabbedOverlay;
}
var pointerEvent = {
type: "Move",
id: HARDWARE_MOUSE_ID,
pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point),
pos3D: intersectInfo.point,
normal: intersectInfo.normal,
direction: intersectInfo.searchRay.direction,
button: "NoButtons",
isPrimaryHeld: true
};
var POINTER_PRESS_TO_MOVE_DELAY = 0.15; // seconds
var POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.05; // radians ~ 3 degrees
if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY ||
angleBetween(pointerEvent.direction, this.touchingEnterPointerEvent.direction) > POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE) {
Overlays.sendMouseMoveOnOverlay(this.grabbedOverlay, pointerEvent);
this.deadspotExpired = true;
}
this.intersectionDistance = intersectInfo.distance;
if (farGrabEnabled) {
this.searchIndicatorOn(intersectInfo.searchRay);
}
Reticle.setVisible(false);
} else {
this.setState(STATE_OFF, "grabbed overlay was destroyed");
return;
}
};
this.release = function() {
this.turnOffVisualizations();
@ -2434,6 +2645,7 @@ function MyController(hand) {
this.actionID = null;
this.grabbedEntity = null;
this.grabbedOverlay = null;
this.grabbedHotspot = null;
if (this.triggerSmoothedGrab()) {

View file

@ -334,9 +334,14 @@ input[type=button].red {
}
input[type=button].blue {
color: #fff;
background-color: #94132e;
background-color: #1080b8;
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
}
input[type=button].white {
color: #121212;
background-color: #afafaf;
background: linear-gradient(#fff 20%, #afafaf 100%);
}
input[type=button]:enabled:hover {
background: linear-gradient(#000, #000);
@ -350,6 +355,10 @@ input[type=button].blue:enabled:hover {
background: linear-gradient(#00b4ef, #00b4ef);
border: none;
}
input[type=button].white:enabled:hover {
background: linear-gradient(#fff, #fff);
border: none;
}
input[type=button]:active {
background: linear-gradient(#343434, #343434);
@ -360,6 +369,9 @@ input[type=button].red:active {
input[type=button].blue:active {
background: linear-gradient(#1080b8, #1080b8);
}
input[type=button].white:active {
background: linear-gradient(#afafaf, #afafaf);
}
input[type=button]:disabled {
color: #252525;

View file

@ -22,11 +22,12 @@ body {
margin-bottom: 20px;
}
.marketplaces-intro-text {
margin-bottom: 60px;
margin-bottom: 30px;
}
.marketplace-tile {
float:left;
width: 100%;
margin-bottom: 25px;
}
.marketplace-tile-first-column {
text-align: center;
@ -75,6 +76,44 @@ body {
.marketplace-clara-steps > li {
margin-top: 5px;
}
#marketplace-navigation {
width: 100%;
height: 50px;
background: #00b4ef;
position: fixed;
bottom: 0;
}
#marketplace-navigation .glyph {
/*
// Target look but can't use font in injected script.
font-family: HiFi-Glyphs;
font-size: 40px;
margin-left: 20px;
*/
font-family: sans-serif;
font-size: 24px;
margin-left: 20px;
margin-right: 3px;
color: #fff;
line-height: 50px;
}
#marketplace-navigation .text {
color: #fff;
font-size: 18px;
line-height: 50px;
vertical-align: top;
position: relative;
top: 1px;
}
#marketplace-navigation input {
position: absolute;
right: 20px;
margin-top: 12px;
padding-left: 15px;
padding-right: 15px;
}
@media (max-width:768px) {
.marketplace-tile-first-column {
float: left;
@ -89,10 +128,10 @@ body {
text-align:center;
}
.tile-divider {
width: 100%;
margin-left: 0%;
width: 100%;
margin-left: 0;
}
.marketplace-tile-image {
margin-bottom: 15px;
}
}
.marketplace-tile-image{
margin-bottom:15px;
}
}

View file

@ -56,7 +56,7 @@ function setUpKeyboardControl() {
}
}
var inputs = document.querySelectorAll("input[type=text], input[type=number], textarea");
var inputs = document.querySelectorAll("input[type=text], input[type=password], input[type=number], textarea");
for (var i = 0, length = inputs.length; i < length; i++) {
inputs[i].addEventListener("focus", raiseKeyboard);
inputs[i].addEventListener("blur", lowerKeyboard);

View file

@ -1,12 +0,0 @@
function loaded() {
bindExploreButtons();
}
function bindExploreButtons() {
$('#exploreClaraMarketplace').on('click', function() {
window.location = "https://clara.io/library?public=true"
})
$('#exploreHifiMarketplace').on('click', function() {
window.location = "http://www.highfidelity.com/marketplace"
})
}

View file

@ -0,0 +1,246 @@
//
// marketplacesInject.js
//
// Created by David Rowe on 12 Nov 2016.
// Copyright 2016 High Fidelity, Inc.
//
// Injected into marketplace Web pages.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function () {
// Event bridge messages.
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
var GOTO_DIRECTORY = "GOTO_DIRECTORY";
var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS";
var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS";
var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS";
var canWriteAssets = false;
function injectCommonCode(isDirectoryPage) {
// Supporting styles from marketplaces.css.
// Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain.
$("head").append(
'<style>' +
'#marketplace-navigation { font-family: Arial, Helvetica, sans-serif; width: 100%; height: 50px; background: #00b4ef; position: fixed; bottom: 0; z-index: 1000; }' +
'#marketplace-navigation .glyph { margin-left: 20px; margin-right: 3px; font-family: sans-serif; color: #fff; font-size: 24px; line-height: 50px; }' +
'#marketplace-navigation .text { color: #fff; font-size: 18px; line-height: 50px; vertical-align: top; position: relative; top: 1px; }' +
'#marketplace-navigation input#back-button { position: absolute; left: 20px; margin-top: 12px; padding-left: 0; padding-right: 5px; }' +
'#marketplace-navigation input#all-markets { position: absolute; right: 20px; margin-top: 12px; padding-left: 15px; padding-right: 15px; }' +
'#marketplace-navigation .right { position: absolute; right: 20px; }' +
'</style>'
);
// Supporting styles from edit-style.css.
// Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain.
$("head").append(
'<style>' +
'input[type=button] { font-family: Arial, Helvetica, sans-serif; font-weight: bold; font-size: 12px; text-transform: uppercase; vertical-align: center; height: 28px; min-width: 100px; padding: 0 15px; border-radius: 5px; border: none; color: #fff; background-color: #000; background: linear-gradient(#343434 20%, #000 100%); cursor: pointer; }' +
'input[type=button].white { color: #121212; background-color: #afafaf; background: linear-gradient(#fff 20%, #afafaf 100%); }' +
'input[type=button].white:enabled:hover { background: linear-gradient(#fff, #fff); border: none; }' +
'input[type=button].white:active { background: linear-gradient(#afafaf, #afafaf); }' +
'</style>'
);
// Footer.
var isInitialHiFiPage = location.href === "https://metaverse.highfidelity.com/marketplace?";
$("body").append(
'<div id="marketplace-navigation">' +
(!isInitialHiFiPage ? '<input id="back-button" type="button" class="white" value="&lt; Back" />' : '') +
(isInitialHiFiPage ? '<span class="glyph">&#x1f6c8;</span> <span class="text">See also other marketplaces.</span>' : '') +
(!isDirectoryPage ? '<input id="all-markets" type="button" class="white" value="See All Markets" />' : '') +
(isDirectoryPage ? '<span class="right"><span class="glyph">&#x1f6c8;</span> <span class="text">Select a marketplace to explore.</span><span>' : '') +
'</div>'
);
// Footer actions.
$("#back-button").on("click", function () {
window.history.back();
});
$("#all-markets").on("click", function () {
EventBridge.emitWebEvent(GOTO_DIRECTORY);
});
}
function injectDirectoryCode() {
// Remove e-mail hyperlink.
var letUsKnow = $("#letUsKnow");
letUsKnow.replaceWith(letUsKnow.html());
// Add button links.
$('#exploreClaraMarketplace').on('click', function () {
window.location = "https://clara.io/library?gameCheck=true&public=true"
});
$('#exploreHifiMarketplace').on('click', function () {
window.location = "http://www.highfidelity.com/marketplace"
});
}
function injectHiFiCode() {
// Nothing to do.
}
function updateClaraCode() {
// Have to repeatedly update Clara page because its content can change dynamically without location.href changing.
// Clara library page.
if (location.href.indexOf("clara.io/library") !== -1) {
// Make entries navigate to "Image" view instead of default "Real Time" view.
var elements = $("a.thumbnail");
for (var i = 0, length = elements.length; i < length; i++) {
var value = elements[i].getAttribute("href");
if (value.slice(-6) !== "/image") {
elements[i].setAttribute("href", value + "/image");
}
}
}
// Clara item page.
if (location.href.indexOf("clara.io/view/") !== -1) {
// Make site navigation links retain gameCheck etc. parameters.
var element = $("a[href^=\'/library\']")[0];
var parameters = "?gameCheck=true&public=true";
var href = element.getAttribute("href");
if (href.slice(-parameters.length) !== parameters) {
element.setAttribute("href", href + parameters);
}
// Replace download options with a single, "Download to High Fidelity" option.
var buttons = $("a.embed-button").parent("div");
if (buttons.length > 0) {
var downloadFBX = buttons.find("a[data-extension=\'fbx\']")[0];
downloadFBX.addEventListener("click", startAutoDownload);
var firstButton = buttons.children(":first-child")[0];
buttons[0].insertBefore(downloadFBX, firstButton);
downloadFBX.setAttribute("class", "btn btn-primary download");
downloadFBX.innerHTML = "<i class=\'glyphicon glyphicon-download-alt\'></i> Download to High Fidelity";
buttons.children(":nth-child(2), .btn-group , .embed-button").each(function () { this.remove(); });
}
// Move the "Download to High Fidelity" button to be more visible on tablet.
if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) {
// Moving the button stops the Clara.io download from starting so instead, make a visual copy in the right place
// and wire its click event to click the original button.
var downloadContainer = $('<div id="hifi-download-container"></div>');
$(".top-title .col-sm-4").append(downloadContainer);
var downloadButton = $("a[data-extension=\'fbx\']").clone();
downloadButton[0].addEventListener("click", function () { downloadFBX.click(); });
downloadContainer.append(downloadButton);
downloadFBX.style.visibility = "hidden";
}
// Automatic download to High Fidelity.
var downloadTimer;
function startAutoDownload(event) {
if (!canWriteAssets) {
console.log("Clara.io FBX file download cancelled because no permissions to write to Asset Server");
EventBridge.emitWebEvent(WARN_USER_NO_PERMISSIONS);
event.stopPropagation();
}
window.scrollTo(0, 0); // Scroll to top ready for history.back().
if (!downloadTimer) {
downloadTimer = setInterval(autoDownload, 1000);
}
}
function autoDownload() {
if ($("div.download-body").length !== 0) {
var downloadButton = $("div.download-body a.download-file");
if (downloadButton.length > 0) {
clearInterval(downloadTimer);
downloadTimer = null;
var href = downloadButton[0].href;
EventBridge.emitWebEvent(CLARA_IO_DOWNLOAD + " " + href);
console.log("Clara.io FBX file download initiated for " + href);
$("a.btn.cancel").click();
history.back(); // Remove history item created by clicking "download".
};
} else if ($("div#view-signup_login_dialog").length === 0) {
// Don't stop checking for button if user is asked to log in.
clearInterval(downloadTimer);
downloadTimer = null;
}
}
}
}
function injectClaraCode() {
// Make space for marketplaces footer in Clara pages.
$("head").append(
'<style>' +
'#app { margin-bottom: 135px; }' +
'.footer { bottom: 50px; }' +
'</style>'
);
// Condense space.
$("head").append(
'<style>' +
'div.page-title { line-height: 1.2; font-size: 13px; }' +
'div.page-title-row { padding-bottom: 0; }' +
'</style>'
);
// Move "Download to High Fidelity" button.
$("head").append(
'<style>' +
'#hifi-download-container { position: absolute; top: 6px; right: 16px; }' +
'</style>'
);
// Update code injected per page displayed.
var updateClaraCodeInterval = undefined;
updateClaraCode();
updateClaraCodeInterval = setInterval(function () {
updateClaraCode();
}, 1000);
window.addEventListener("unload", function () {
clearInterval(updateClaraCodeInterval);
updateClaraCodeInterval = undefined;
});
EventBridge.emitWebEvent(QUERY_CAN_WRITE_ASSETS);
}
function onLoad() {
EventBridge.scriptEventReceived.connect(function (message) {
if (message.slice(0, CAN_WRITE_ASSETS.length) === CAN_WRITE_ASSETS) {
canWriteAssets = message.slice(-4) === "true";
}
});
var DIRECTORY = 0;
var HIFI = 1;
var CLARA = 2;
var pageType = DIRECTORY;
if (location.href.indexOf("highfidelity.com/") !== -1) { pageType = HIFI; }
if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; }
injectCommonCode(pageType === DIRECTORY);
switch (pageType) {
case DIRECTORY:
injectDirectoryCode();
break;
case HIFI:
injectHiFiCode();
break;
case CLARA:
injectClaraCode();
break;
}
}
// Load / unload.
window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready().
}());

View file

@ -13,19 +13,12 @@
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
<link rel="stylesheet" type="text/css" href="css/marketplaces.css">
<script src="js/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script src="js/marketplaces.js"></script>
</head>
<body onload='loaded();'>
<body>
<div class="marketplaces-container">
<h2 class="marketplaces-title">
Marketplaces
</h2>
<h2 class="marketplaces-title">Marketplaces</h2>
<div class="marketplaces-intro-text">
<p>
You can bring content into High Fidelity forom anywhere you want. Here are a few places that support direct import of content right now. If you'd like to suggest a Market to include here, <a href="mailto:contact@highfidelity.io">let us know.</a>
</p>
<p>You can bring content into High Fidelity from anywhere you want. Here are a few places that support direct import of content right now. If you'd like to suggest a Market to include here, <a id="letUsKnow" href="mailto:contact@highfidelity.io">let us know.</a></p>
</div>
<div class="marketplace-tile">
<div class="marketplace-tile-first-column">
@ -34,7 +27,7 @@
<div class="marketplace-tile-second-column">
<p class="marketplace-tile-description">This is the default High Fidelity marketplace. Viewing and downloading content from here is fully supported in Interface.</p>
<div class="exploreButton-holder">
<input class="blue exploreButton" type="button" value="Explore" id="exploreHifiMarketplace"></input></div>
<input class="blue exploreButton" type="button" value="Explore" id="exploreHifiMarketplace" />
</div>
<hr class="tile-divider">
</div>
@ -45,21 +38,21 @@
<div class="marketplace-tile-second-column">
<p class="marketplace-tile-description">Clara.io has thousands of models available for importing into High Fidelity. Follow these steps for the best experience:</p>
<ol class="marketplace-clara-steps">
<li><a href="http://www.clara.io/signup">Create an account here </a>or log in as an existing user.</li>
<li>Choose a model from the list and click Download -> Autodesk FBX.</li>
<li>After the file processes, click Download.</li>
<li>Add the model to your asset server, then find it from the list and choose Add To World.</li>
<li><a id="claraSignUp" href="http://www.clara.io/signup">Create an account here </a>or log in as an existing user.</li>
<li>Choose a model from the list and click &ldquo;Download to High Fidelity&rdquo;.</li>
</ol>
<div class="exploreButton-holder">
<input class="blue exploreButton" type="button" value="Explore" id="exploreClaraMarketplace"></input>
<input class="blue exploreButton" type="button" value="Explore" id="exploreClaraMarketplace" />
</div>
</div>
<hr class="tile-divider">
</div>
</div>
</div>
<!-- Code similar to the following is injected; the following is included here to help with CSS styling.
<div id="marketplace-navigation">
<span class="glyph">&#x1f6c8;</span> <span class="text">Select a marketplace to explore.</span>
<input id="all-markets" type="button" class="white" value="See All Markets" />
</div>
-->
</body>
</html>

View file

@ -11,7 +11,7 @@
var RAD_TO_DEG = 180 / Math.PI;
var X_AXIS = {x: 1, y: 0, z: 0};
var Y_AXIS = {x: 0, y: 1, z: 0};
var DEFAULT_DPI = 32;
var DEFAULT_DPI = 30;
var DEFAULT_WIDTH = 0.5;
var TABLET_URL = "https://s3.amazonaws.com/hifi-public/tony/tablet.fbx";
@ -43,14 +43,16 @@ WebTablet = function (url, width, dpi, clientOnly) {
var ASPECT = 4.0 / 3.0;
var WIDTH = width || DEFAULT_WIDTH;
var HEIGHT = WIDTH * ASPECT;
var TABLET_HEIGHT_SCALE = 640 / 680; // Screen size of tablet entity isn't quite the desired aspect.
var HEIGHT = WIDTH * ASPECT * TABLET_HEIGHT_SCALE;
var DEPTH = 0.025;
var DPI = dpi || DEFAULT_DPI;
var SENSOR_TO_ROOM_MATRIX = -2;
var spawnInfo = calcSpawnInfo();
var tabletEntityPosition = spawnInfo.position;
var tabletEntityRotation = spawnInfo.rotation;
this.tabletEntityID = Entities.addEntity({
name: "tablet",
type: "Model",
@ -62,40 +64,50 @@ WebTablet = function (url, width, dpi, clientOnly) {
}),
dimensions: {x: WIDTH, y: HEIGHT, z: DEPTH},
parentID: MyAvatar.sessionUUID,
parentJointIndex: -2
parentJointIndex: SENSOR_TO_ROOM_MATRIX
}, clientOnly);
var WEB_ENTITY_REDUCTION_FACTOR = {x: 0.78, y: 0.85};
var WEB_ENTITY_Z_OFFSET = -0.01;
var WEB_OVERLAY_Z_OFFSET = -0.01;
var webEntityRotation = Quat.multiply(spawnInfo.rotation, Quat.angleAxis(180, Y_AXIS));
var webEntityPosition = Vec3.sum(spawnInfo.position, Vec3.multiply(WEB_ENTITY_Z_OFFSET, Quat.getFront(webEntityRotation)));
var webOverlayRotation = Quat.multiply(spawnInfo.rotation, Quat.angleAxis(180, Y_AXIS));
var webOverlayPosition = Vec3.sum(spawnInfo.position, Vec3.multiply(WEB_OVERLAY_Z_OFFSET, Quat.getFront(webOverlayRotation)));
this.webEntityID = Entities.addEntity({
name: "web",
type: "Web",
sourceUrl: url,
dimensions: {x: WIDTH * WEB_ENTITY_REDUCTION_FACTOR.x,
y: HEIGHT * WEB_ENTITY_REDUCTION_FACTOR.y,
z: 0.1},
position: webEntityPosition,
rotation: webEntityRotation,
shapeType: "box",
this.webOverlayID = Overlays.addOverlay("web3d", {
url: url,
position: webOverlayPosition,
rotation: webOverlayRotation,
resolution: { x: 480, y: 640 },
dpi: DPI,
color: { red: 255, green: 255, blue: 255 },
alpha: 1.0,
parentID: this.tabletEntityID,
parentJointIndex: -1
}, clientOnly);
});
this.state = "idle";
};
WebTablet.prototype.setURL = function (url) {
Overlays.editOverlay(this.webOverlayID, { url: url });
};
WebTablet.prototype.setScriptURL = function (scriptURL) {
Overlays.editOverlay(this.webOverlayID, { scriptURL: scriptURL });
};
WebTablet.prototype.getOverlayObject = function () {
return Overlays.getOverlayObject(this.webOverlayID);
};
WebTablet.prototype.destroy = function () {
Entities.deleteEntity(this.webEntityID);
Overlays.deleteOverlay(this.webOverlayID);
Entities.deleteEntity(this.tabletEntityID);
};
WebTablet.prototype.pickle = function () {
return JSON.stringify({webEntityID: this.webEntityID, tabletEntityID: this.tabletEntityID});
return JSON.stringify({ webOverlayID: this.webOverlayID, tabletEntityID: this.tabletEntityID });
};
WebTablet.unpickle = function (string) {
if (!string) {
return;

View file

@ -1,85 +0,0 @@
"use strict";
//
// clara.js
//
// Created by Eric Levin on 8 Jan 2016
// Edited by Elisa Lupin-Jimenez on 23 Aug 2016
// Copyright 2016 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() { // BEGIN LOCAL_SCOPE
var toolIconUrl = Script.resolvePath("../assets/images/tools/");
var qml = Script.resolvePath("../../../resources/qml/Marketplaces.qml")
var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
var marketplaceWindow = new OverlayWindow({
title: "Marketplace",
source: qml,
width: 1000,
height: 900,
toolWindow: false,
visible: false,
});
var toolHeight = 50;
var toolWidth = 50;
var TOOLBAR_MARGIN_Y = 0;
function showMarketplace(marketplaceID) {
var url = MARKETPLACE_URL;
if (marketplaceID) {
url = url + "/items/" + marketplaceID;
}
marketplaceWindow.setVisible(true);
UserActivityLogger.openedMarketplace();
}
function hideMarketplace() {
marketplaceWindow.setVisible(false);
}
function toggleMarketplace() {
if (marketplaceWindow.visible) {
hideMarketplace();
} else {
showMarketplace();
}
}
var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
var browseExamplesButton = toolBar.addButton({
imageURL: toolIconUrl + "market.svg",
objectName: "marketplace",
buttonState: 1,
defaultState: 1,
hoverState: 3,
alpha: 0.9
});
function onExamplesWindowVisibilityChanged() {
browseExamplesButton.writeProperty('buttonState', marketplaceWindow.visible ? 0 : 1);
browseExamplesButton.writeProperty('defaultState', marketplaceWindow.visible ? 0 : 1);
browseExamplesButton.writeProperty('hoverState', marketplaceWindow.visible ? 2 : 3);
}
function onClick() {
toggleMarketplace();
}
browseExamplesButton.clicked.connect(onClick);
marketplaceWindow.visibleChanged.connect(onExamplesWindowVisibilityChanged);
Script.scriptEnding.connect(function () {
toolBar.removeButton("marketplace");
browseExamplesButton.clicked.disconnect(onClick);
marketplaceWindow.visibleChanged.disconnect(onExamplesWindowVisibilityChanged);
});
}()); // END LOCAL_SCOPE

View file

@ -0,0 +1,178 @@
//
// marketplaces.js
//
// Created by Eric Levin on 8 Jan 2016
// Copyright 2016 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() { // BEGIN LOCAL_SCOPE
/* global WebTablet */
Script.include("../libraries/WebTablet.js");
var toolIconUrl = Script.resolvePath("../assets/images/tools/");
var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page.
var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html");
var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js");
// Event bridge messages.
var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD";
var GOTO_DIRECTORY = "GOTO_DIRECTORY";
var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS";
var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS";
var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS";
var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server.";
var marketplaceWindow = new OverlayWebWindow({
title: "Marketplace",
source: "about:blank",
width: 900,
height: 700,
visible: false
});
marketplaceWindow.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL);
marketplaceWindow.webEventReceived.connect(function (message) {
if (message === GOTO_DIRECTORY) {
marketplaceWindow.setURL(MARKETPLACES_URL);
}
if (message === QUERY_CAN_WRITE_ASSETS) {
marketplaceWindow.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets());
}
if (message === WARN_USER_NO_PERMISSIONS) {
Window.alert(NO_PERMISSIONS_ERROR_MESSAGE);
}
});
var toolHeight = 50;
var toolWidth = 50;
var TOOLBAR_MARGIN_Y = 0;
var marketplaceVisible = false;
var marketplaceWebTablet;
// We persist clientOnly data in the .ini file, and reconstitute it on restart.
// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change.
var persistenceKey = "io.highfidelity.lastDomainTablet";
function shouldShowWebTablet() {
var rightPose = Controller.getPoseValue(Controller.Standard.RightHand);
var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand);
var hasHydra = !!Controller.Hardware.Hydra;
return HMD.active && (leftPose.valid || rightPose.valid || hasHydra);
}
function showMarketplace() {
if (shouldShowWebTablet()) {
updateButtonState(true);
marketplaceWebTablet = new WebTablet(MARKETPLACE_URL_INITIAL, null, null, true);
Settings.setValue(persistenceKey, marketplaceWebTablet.pickle());
marketplaceWebTablet.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL);
marketplaceWebTablet.getOverlayObject().webEventReceived.connect(function (message) {
if (message === GOTO_DIRECTORY) {
marketplaceWebTablet.setURL(MARKETPLACES_URL);
}
if (message === QUERY_CAN_WRITE_ASSETS) {
marketplaceWebTablet.getOverlayObject().emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets());
}
if (message === WARN_USER_NO_PERMISSIONS) {
Window.alert(NO_PERMISSIONS_ERROR_MESSAGE);
}
});
} else {
marketplaceWindow.setURL(MARKETPLACE_URL_INITIAL);
marketplaceWindow.setVisible(true);
}
marketplaceVisible = true;
UserActivityLogger.openedMarketplace();
}
function hideTablet(tablet) {
if (!tablet) {
return;
}
updateButtonState(false);
tablet.destroy();
marketplaceWebTablet = null;
Settings.setValue(persistenceKey, "");
}
function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated
var tablet = WebTablet.unpickle(Settings.getValue(persistenceKey, ""));
hideTablet(tablet);
}
function hideMarketplace() {
if (marketplaceWindow.visible) {
marketplaceWindow.setVisible(false);
marketplaceWindow.setURL("about:blank");
} else if (marketplaceWebTablet) {
hideTablet(marketplaceWebTablet);
}
marketplaceVisible = false;
}
marketplaceWindow.closed.connect(function () {
marketplaceWindow.setURL("about:blank");
});
function toggleMarketplace() {
if (marketplaceVisible) {
hideMarketplace();
} else {
showMarketplace();
}
}
var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
var browseExamplesButton = toolBar.addButton({
imageURL: toolIconUrl + "market.svg",
objectName: "marketplace",
buttonState: 1,
defaultState: 1,
hoverState: 3,
alpha: 0.9
});
function updateButtonState(visible) {
browseExamplesButton.writeProperty('buttonState', visible ? 0 : 1);
browseExamplesButton.writeProperty('defaultState', visible ? 0 : 1);
browseExamplesButton.writeProperty('hoverState', visible ? 2 : 3);
}
function onMarketplaceWindowVisibilityChanged() {
updateButtonState(marketplaceWindow.visible);
marketplaceVisible = marketplaceWindow.visible;
}
function onCanWriteAssetsChanged() {
var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets();
if (marketplaceWindow.visible) {
marketplaceWindow.emitScriptEvent(message);
}
if (marketplaceWebTablet) {
marketplaceWebTablet.getOverlayObject().emitScriptEvent(message);
}
}
function onClick() {
toggleMarketplace();
}
browseExamplesButton.clicked.connect(onClick);
marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged);
Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged);
clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash.
// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}),
// but the HUD version stays around, so lets do the same.
Script.scriptEnding.connect(function () {
toolBar.removeButton("marketplace");
browseExamplesButton.clicked.disconnect(onClick);
marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged);
Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged);
});
}()); // END LOCAL_SCOPE