Merge branch 'master' of github.com:highfidelity/hifi into fix/resort-in-octree

This commit is contained in:
Zach Pomerantz 2016-03-08 10:22:25 -08:00
commit d1324b2e7a
300 changed files with 9689 additions and 3353 deletions
assignment-client/src
cmake
domain-server
examples
ice-server
interface

View file

@ -55,13 +55,7 @@ Agent::Agent(ReceivedMessage& message) :
{
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
auto assetClient = DependencyManager::set<AssetClient>();
QThread* assetThread = new QThread;
assetThread->setObjectName("Asset Thread");
assetClient->moveToThread(assetThread);
connect(assetThread, &QThread::started, assetClient.data(), &AssetClient::init);
assetThread->start();
ResourceManager::init();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
@ -471,11 +465,7 @@ void Agent::aboutToFinish() {
// our entity tree is going to go away so tell that to the EntityScriptingInterface
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
// cleanup the AssetClient thread
QThread* assetThread = DependencyManager::get<AssetClient>()->thread();
DependencyManager::destroy<AssetClient>();
assetThread->quit();
assetThread->wait();
ResourceManager::cleanup();
// cleanup the AudioInjectorManager (and any still running injectors)
DependencyManager::destroy<AudioInjectorManager>();

View file

@ -139,13 +139,13 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
httpStatusPort = parser.value(httpStatusPortOption).toUShort();
}
QDir logDirectory { "." };
QString logDirectory;
if (parser.isSet(logDirectoryOption)) {
logDirectory = parser.value(logDirectoryOption);
} else {
logDirectory = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
}
Assignment::Type requestAssignmentType = Assignment::AllTypes;
if (argumentVariantMap.contains(ASSIGNMENT_TYPE_OVERRIDE_OPTION)) {
requestAssignmentType = (Assignment::Type) argumentVariantMap.value(ASSIGNMENT_TYPE_OVERRIDE_OPTION).toInt();

View file

@ -33,8 +33,7 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
const unsigned int maxAssignmentClientForks,
Assignment::Type requestAssignmentType, QString assignmentPool,
quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort, quint16 httpStatusServerPort, QDir logDirectory) :
_logDirectory(logDirectory),
quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory) :
_httpManager(QHostAddress::LocalHost, httpStatusServerPort, "", this),
_numAssignmentClientForks(numAssignmentClientForks),
_minAssignmentClientForks(minAssignmentClientForks),
@ -48,6 +47,11 @@ AssignmentClientMonitor::AssignmentClientMonitor(const unsigned int numAssignmen
{
qDebug() << "_requestAssignmentType =" << _requestAssignmentType;
if (!logDirectory.isEmpty()) {
_wantsChildFileLogging = true;
_logDirectory = QDir(logDirectory);
}
// start the Logging class with the parent's target name
LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_MONITOR_TARGET_NAME);
@ -159,52 +163,61 @@ void AssignmentClientMonitor::spawnChildClient() {
_childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION);
_childArguments.append(QString::number(DependencyManager::get<NodeList>()->getLocalSockAddr().getPort()));
// Setup log files
const QString DATETIME_FORMAT = "yyyyMMdd.hh.mm.ss.zzz";
QString nowString, stdoutFilenameTemp, stderrFilenameTemp, stdoutPathTemp, stderrPathTemp;
if (!_logDirectory.exists()) {
qDebug() << "Log directory (" << _logDirectory.absolutePath() << ") does not exist, creating.";
_logDirectory.mkpath(_logDirectory.absolutePath());
if (_wantsChildFileLogging) {
// Setup log files
const QString DATETIME_FORMAT = "yyyyMMdd.hh.mm.ss.zzz";
if (!_logDirectory.exists()) {
qDebug() << "Log directory (" << _logDirectory.absolutePath() << ") does not exist, creating.";
_logDirectory.mkpath(_logDirectory.absolutePath());
}
nowString = QDateTime::currentDateTime().toString(DATETIME_FORMAT);
stdoutFilenameTemp = QString("ac-%1-stdout.txt").arg(nowString);
stderrFilenameTemp = QString("ac-%1-stderr.txt").arg(nowString);
stdoutPathTemp = _logDirectory.absoluteFilePath(stdoutFilenameTemp);
stderrPathTemp = _logDirectory.absoluteFilePath(stderrFilenameTemp);
// reset our output and error files
assignmentClient->setStandardOutputFile(stdoutPathTemp);
assignmentClient->setStandardErrorFile(stderrPathTemp);
}
auto nowString = QDateTime::currentDateTime().toString(DATETIME_FORMAT);
auto stdoutFilenameTemp = QString("ac-%1-stdout.txt").arg(nowString);
auto stderrFilenameTemp = QString("ac-%1-stderr.txt").arg(nowString);
QString stdoutPathTemp = _logDirectory.absoluteFilePath(stdoutFilenameTemp);
QString stderrPathTemp = _logDirectory.absoluteFilePath(stderrFilenameTemp);
// reset our output and error files
assignmentClient->setStandardOutputFile(stdoutPathTemp);
assignmentClient->setStandardErrorFile(stderrPathTemp);
// make sure that the output from the child process appears in our output
assignmentClient->setProcessChannelMode(QProcess::ForwardedChannels);
assignmentClient->start(QCoreApplication::applicationFilePath(), _childArguments);
// Update log path to use PID in filename
auto stdoutFilename = QString("ac-%1_%2-stdout.txt").arg(nowString).arg(assignmentClient->processId());
auto stderrFilename = QString("ac-%1_%2-stderr.txt").arg(nowString).arg(assignmentClient->processId());
QString stdoutPath = _logDirectory.absoluteFilePath(stdoutFilename);
QString stderrPath = _logDirectory.absoluteFilePath(stderrFilename);
QString stdoutPath, stderrPath;
qDebug() << "Renaming " << stdoutPathTemp << " to " << stdoutPath;
if (!_logDirectory.rename(stdoutFilenameTemp, stdoutFilename)) {
qDebug() << "Failed to rename " << stdoutFilenameTemp;
stdoutPath = stdoutPathTemp;
stdoutFilename = stdoutFilenameTemp;
if (_wantsChildFileLogging) {
// Update log path to use PID in filename
auto stdoutFilename = QString("ac-%1_%2-stdout.txt").arg(nowString).arg(assignmentClient->processId());
auto stderrFilename = QString("ac-%1_%2-stderr.txt").arg(nowString).arg(assignmentClient->processId());
stdoutPath = _logDirectory.absoluteFilePath(stdoutFilename);
stderrPath = _logDirectory.absoluteFilePath(stderrFilename);
qDebug() << "Renaming " << stdoutPathTemp << " to " << stdoutPath;
if (!_logDirectory.rename(stdoutFilenameTemp, stdoutFilename)) {
qDebug() << "Failed to rename " << stdoutFilenameTemp;
stdoutPath = stdoutPathTemp;
stdoutFilename = stdoutFilenameTemp;
}
qDebug() << "Renaming " << stderrPathTemp << " to " << stderrPath;
if (!QFile::rename(stderrPathTemp, stderrPath)) {
qDebug() << "Failed to rename " << stderrFilenameTemp;
stderrPath = stderrPathTemp;
stderrFilename = stderrFilenameTemp;
}
qDebug() << "Child stdout being written to: " << stdoutFilename;
qDebug() << "Child stderr being written to: " << stderrFilename;
}
qDebug() << "Renaming " << stderrPathTemp << " to " << stderrPath;
if (!QFile::rename(stderrPathTemp, stderrPath)) {
qDebug() << "Failed to rename " << stderrFilenameTemp;
stderrPath = stderrPathTemp;
stderrFilename = stderrFilenameTemp;
}
qDebug() << "Child stdout being written to: " << stdoutFilename;
qDebug() << "Child stderr being written to: " << stderrFilename;
if (assignmentClient->processId() > 0) {
auto pid = assignmentClient->processId();
// make sure we hear that this process has finished when it does
@ -212,6 +225,7 @@ void AssignmentClientMonitor::spawnChildClient() {
this, [this, pid]() { childProcessFinished(pid); });
qDebug() << "Spawned a child client with PID" << assignmentClient->processId();
_childProcesses.insert(assignmentClient->processId(), { assignmentClient, stdoutPath, stderrPath });
}
}

View file

@ -38,7 +38,7 @@ public:
AssignmentClientMonitor(const unsigned int numAssignmentClientForks, const unsigned int minAssignmentClientForks,
const unsigned int maxAssignmentClientForks, Assignment::Type requestAssignmentType,
QString assignmentPool, quint16 listenPort, QUuid walletUUID, QString assignmentServerHostname,
quint16 assignmentServerPort, quint16 httpStatusServerPort, QDir logDirectory);
quint16 assignmentServerPort, quint16 httpStatusServerPort, QString logDirectory);
~AssignmentClientMonitor();
void stopChildProcesses();
@ -73,6 +73,8 @@ private:
quint16 _assignmentServerPort;
QMap<qint64, ACProcess> _childProcesses;
bool _wantsChildFileLogging { false };
};
#endif // hifi_AssignmentClientMonitor_h

View file

@ -277,14 +277,17 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
// set of stats to have, but we'd probably want a different data structure if we keep it very long.
// Since this version uses a single shared QMap for all senders, there could be some lock contention
// on this QWriteLocker
void EntityServer::trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& viewerNode) {
void EntityServer::trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) {
QWriteLocker locker(&_viewerSendingStatsLock);
_viewerSendingStats[viewerNode][dataID] = { usecTimestampNow(), dataLastEdited };
_viewerSendingStats[sessionID][dataID] = { usecTimestampNow(), dataLastEdited };
}
void EntityServer::trackViewerGone(const QUuid& viewerNode) {
void EntityServer::trackViewerGone(const QUuid& sessionID) {
QWriteLocker locker(&_viewerSendingStatsLock);
_viewerSendingStats.remove(viewerNode);
_viewerSendingStats.remove(sessionID);
if (_entitySimulation) {
_entitySimulation->clearOwnership(sessionID);
}
}
QString EntityServer::serverSubclassStats() {

View file

@ -27,6 +27,8 @@ struct ViewerSendingStats {
quint64 lastEdited;
};
class SimpleEntitySimulation;
class EntityServer : public OctreeServer, public NewlyCreatedEntityHook {
Q_OBJECT
public:
@ -52,8 +54,8 @@ public:
virtual void readAdditionalConfiguration(const QJsonObject& settingsSectionObject) override;
virtual QString serverSubclassStats() override;
virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& viewerNode) override;
virtual void trackViewerGone(const QUuid& viewerNode) override;
virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) override;
virtual void trackViewerGone(const QUuid& sessionID) override;
public slots:
void pruneDeletedEntities();
@ -65,7 +67,7 @@ private slots:
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
private:
EntitySimulation* _entitySimulation;
SimpleEntitySimulation* _entitySimulation;
QTimer* _pruneDeletedEntitiesTimer = nullptr;
QReadWriteLock _viewerSendingStatsLock;

View file

@ -236,10 +236,6 @@ OctreeServer::OctreeServer(ReceivedMessage& message) :
{
_averageLoopTime.updateAverage(0);
qDebug() << "Octree server starting... [" << this << "]";
// make sure the AccountManager has an Auth URL for payment redemptions
AccountManager::getInstance().setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL);
}
OctreeServer::~OctreeServer() {

View file

@ -64,11 +64,10 @@ macro(install_beside_console)
)
endif()
# set variables used by manual ssleay library copy
set(TARGET_INSTALL_DIR ${COMPONENT_INSTALL_DIR})
set(TARGET_INSTALL_COMPONENT ${SERVER_COMPONENT})
manually_install_openssl_for_qt()
endif ()
# set variables used by manual ssleay library copy
set(TARGET_INSTALL_DIR ${COMPONENT_INSTALL_DIR})
set(TARGET_INSTALL_COMPONENT ${SERVER_COMPONENT})
manually_install_ssl_eay()
endmacro()

View file

@ -0,0 +1,34 @@
#
# ManuallyInstallOpenSSLforQt.cmake
#
# Created by Stephen Birarda on 1/15/16.
# Copyright 2014 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
#
macro(manually_install_openssl_for_qt)
# Qt dynamically links OpenSSL if it can find it on the user's machine
# We want to avoid it being found somewhere random and have it not being a compatible version
# So even though we don't need the dynamic version of OpenSSL for our direct-use purposes
# we use this macro to include the two SSL DLLs with the targets using QtNetwork
if (WIN32)
# we have to call find_package(OpenSSL) here even though this target may not directly need it
find_package(OpenSSL REQUIRED)
install(
FILES "${OPENSSL_DLL_PATH}/ssleay32.dll"
DESTINATION ${TARGET_INSTALL_DIR}
COMPONENT ${TARGET_INSTALL_COMPONENT}
)
install(
FILES "${OPENSSL_DLL_PATH}/libeay32.dll"
DESTINATION ${TARGET_INSTALL_DIR}
COMPONENT ${TARGET_INSTALL_COMPONENT}
)
endif()
endmacro()

View file

@ -1,28 +0,0 @@
#
# ManuallyInstallSSLEay.cmake
#
# Created by Stephen Birarda on 1/15/16.
# Copyright 2014 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
#
macro(manually_install_ssl_eay)
# on windows we have had issues with targets missing ssleay library
# not convinced we actually need it (I assume it would show up in the dependency tree)
# but to be safe we install it manually beside the current target
if (WIN32)
# we need to find SSL_EAY_LIBRARY_* so we can install it beside this target
# so we have to call find_package(OpenSSL) here even though this target may not specifically need it
find_package(OpenSSL REQUIRED)
install(
FILES "${OPENSSL_DLL_PATH}/ssleay32.dll"
DESTINATION ${TARGET_INSTALL_DIR}
COMPONENT ${TARGET_INSTALL_COMPONENT}
)
endif()
endmacro()

View file

@ -67,10 +67,7 @@ find_path(OPENSSL_INCLUDE_DIR NAMES openssl/ssl.h HINTS ${_OPENSSL_ROOT_HINTS_AN
if (WIN32 AND NOT CYGWIN)
if (MSVC)
# /MD and /MDd are the standard values - if someone wants to use
# others, the libnames have to change here too
# use also ssl and ssleay32 in debug as fallback for openssl < 0.9.8b
# TODO: handle /MT and static lib
# In Visual C++ naming convention each of these four kinds of Windows libraries has it's standard suffix:
# * MD for dynamic-release
# * MDd for dynamic-debug
@ -81,20 +78,23 @@ if (WIN32 AND NOT CYGWIN)
# We are using the libraries located in the VC subdir instead of the parent directory eventhough :
# libeay32MD.lib is identical to ../libeay32.lib, and
# ssleay32MD.lib is identical to ../ssleay32.lib
find_library(LIB_EAY_DEBUG NAMES libeay32MDd libeay32d
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC"
# The Kitware FindOpenSSL module has been modified here by High Fidelity to look specifically for static libraries
find_library(LIB_EAY_DEBUG NAMES libeay32MTd
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib/VC/static"
)
find_library(LIB_EAY_RELEASE NAMES libeay32MD libeay32
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC"
find_library(LIB_EAY_RELEASE NAMES libeay32MT
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib/VC/static"
)
find_library(SSL_EAY_DEBUG NAMES ssleay32MDd ssleay32d
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC"
find_library(SSL_EAY_DEBUG NAMES ssleay32MTd
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib/VC/static"
)
find_library(SSL_EAY_RELEASE NAMES ssleay32MD ssleay32 ssl
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "VC" "lib/VC"
find_library(SSL_EAY_RELEASE NAMES ssleay32MT
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib/VC/static"
)
set(LIB_EAY_LIBRARY_DEBUG "${LIB_EAY_DEBUG}")
@ -109,37 +109,6 @@ if (WIN32 AND NOT CYGWIN)
set(OPENSSL_LIBRARIES ${SSL_EAY_LIBRARY} ${LIB_EAY_LIBRARY})
find_path(OPENSSL_DLL_PATH NAMES ssleay32.dll PATH_SUFFIXES "bin" ${_OPENSSL_ROOT_HINTS_AND_PATHS})
elseif (MINGW)
# same player, for MinGW
set(LIB_EAY_NAMES libeay32)
set(SSL_EAY_NAMES ssleay32)
if (CMAKE_CROSSCOMPILING)
list(APPEND LIB_EAY_NAMES crypto)
list(APPEND SSL_EAY_NAMES ssl)
endif ()
find_library(LIB_EAY NAMES ${LIB_EAY_NAMES}
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "lib/MinGW"
)
find_library(SSL_EAY NAMES ${SSL_EAY_NAMES}
${_OPENSSL_ROOT_HINTS_AND_PATHS} PATH_SUFFIXES "lib" "lib/MinGW"
)
mark_as_advanced(SSL_EAY LIB_EAY)
set(OPENSSL_LIBRARIES ${SSL_EAY} ${LIB_EAY})
unset(LIB_EAY_NAMES)
unset(SSL_EAY_NAMES)
else ()
# Not sure what to pick for -say- intel, let's use the toplevel ones and hope someone report issues:
find_library(LIB_EAY NAMES libeay32 HINTS ${_OPENSSL_ROOT_HINTS_AND_PATHS} ${_OPENSSL_LIBDIR} PATH_SUFFIXES lib)
find_library(SSL_EAY NAMES ssleay32 HINTS ${_OPENSSL_ROOT_HINTS_AND_PATHS} ${_OPENSSL_LIBDIR} PATH_SUFFIXES lib)
mark_as_advanced(SSL_EAY LIB_EAY)
set(OPENSSL_LIBRARIES ${SSL_EAY} ${LIB_EAY})
endif()
else()
@ -250,8 +219,4 @@ else ()
)
endif ()
if (WIN32)
add_paths_to_fixup_libs(${OPENSSL_DLL_PATH})
endif ()
mark_as_advanced(OPENSSL_INCLUDE_DIR OPENSSL_LIBRARIES OPENSSL_SEARCH_DIRS)

View file

@ -778,7 +778,7 @@ function chooseFromHighFidelityDomains(clickedButton) {
function createTemporaryDomain() {
swal({
title: 'Create temporary place name',
text: "This will create a temporary place name and domain ID (valid for 30 days)"
text: "This will create a temporary place name and domain ID"
+ " so other users can easily connect to your domain.</br></br>"
+ "In order to make your domain reachable, this will also enable full automatic networking.",
showCancelButton: true,

View file

@ -258,9 +258,26 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
if (onlyEditorsAreRezzers) {
canRez = isAllowedEditor;
}
QUuid hintNodeID;
// in case this is a node that's failing to connect
// double check we don't have a node whose sockets match exactly already in the list
limitedNodeList->eachNodeBreakable([&nodeConnection, &hintNodeID](const SharedNodePointer& node){
if (node->getPublicSocket() == nodeConnection.publicSockAddr
&& node->getLocalSocket() == nodeConnection.localSockAddr) {
// we have a node that already has these exact sockets - this occurs if a node
// is unable to connect to the domain
hintNodeID = node->getUUID();
return false;
}
return true;
});
// add the new node
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
// add the connecting node (or re-use the matched one from eachNodeBreakable above)
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID);
// set the edit rights for this user
newNode->setIsAllowedEditor(isAllowedEditor);
@ -279,28 +296,29 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
return newNode;
}
SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection) {
SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection,
QUuid nodeID) {
HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr;
SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID);
QUuid nodeUUID;
if (connectedPeer) {
// this user negotiated a connection with us via ICE, so re-use their ICE client ID
nodeUUID = nodeConnection.connectUUID;
nodeID = nodeConnection.connectUUID;
if (connectedPeer->getActiveSocket()) {
// set their discovered socket to whatever the activated socket on the network peer object was
discoveredSocket = *connectedPeer->getActiveSocket();
}
} else {
// we got a connectUUID we didn't recognize, just add the node with a new UUID
nodeUUID = QUuid::createUuid();
// we got a connectUUID we didn't recognize, either use the hinted node ID or randomly generate a new one
if (nodeID.isNull()) {
nodeID = QUuid::createUuid();
}
}
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeUUID, nodeConnection.nodeType,
SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType,
nodeConnection.publicSockAddr, nodeConnection.localSockAddr);
// So that we can send messages to this node at will - we need to activate the correct socket on this node now
@ -331,7 +349,6 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
QCryptographicHash::Sha256);
if (rsaPublicKey) {
QByteArray decryptedArray(RSA_size(rsaPublicKey), 0);
int decryptResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
usernameWithToken.size(),

View file

@ -59,7 +59,8 @@ private:
SharedNodePointer processAgentConnectRequest(const NodeConnectionData& nodeConnection,
const QString& username,
const QByteArray& usernameSignature);
SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection);
SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection,
QUuid nodeID = QUuid());
bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);

View file

@ -96,7 +96,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// make sure we hear about newly connected nodes from our gatekeeper
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) {
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
// we either read a certificate and private key or were not passed one
// and completed login or did not need to
@ -198,7 +198,6 @@ bool DomainServer::optionallySetupOAuth() {
}
AccountManager& accountManager = AccountManager::getInstance();
accountManager.disableSettingsFilePersistence();
accountManager.setAuthURL(_oauthProviderURL);
_oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString();
@ -372,20 +371,12 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
packetReceiver.registerListener(PacketType::ICEPing, &_gatekeeper, "processICEPingPacket");
packetReceiver.registerListener(PacketType::ICEPingReply, &_gatekeeper, "processICEPingReplyPacket");
packetReceiver.registerListener(PacketType::ICEServerPeerInformation, &_gatekeeper, "processICEPeerInformationPacket");
packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket");
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
}
bool DomainServer::didSetupAccountManagerWithAccessToken() {
if (AccountManager::getInstance().hasValidAccessToken()) {
// we already gave the account manager a valid access token
return true;
}
return resetAccountManagerAccessToken();
}
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
bool DomainServer::resetAccountManagerAccessToken() {
@ -401,9 +392,13 @@ bool DomainServer::resetAccountManagerAccessToken() {
if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) {
accessToken = accessTokenVariant->toString();
} else {
qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present."
<< "Set an access token via the web interface, in your user or master config"
qDebug() << "A domain-server feature that requires authentication is enabled but no access token is present.";
qDebug() << "Set an access token via the web interface, in your user or master config"
<< "at keypath metaverse.access_token or in your ENV at key DOMAIN_SERVER_ACCESS_TOKEN";
// clear any existing access token from AccountManager
AccountManager::getInstance().setAccessTokenForCurrentAuthURL(QString());
return false;
}
} else {
@ -429,34 +424,6 @@ bool DomainServer::resetAccountManagerAccessToken() {
}
}
bool DomainServer::optionallySetupAssignmentPayment() {
const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments";
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
if (settingsMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) &&
settingsMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool() &&
didSetupAccountManagerWithAccessToken()) {
qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString());
// assume that the fact we are authing against HF data server means we will pay for assignments
// setup a timer to send transactions to pay assigned nodes every 30 seconds
QTimer* creditSetupTimer = new QTimer(this);
connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits);
const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000;
creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS);
QTimer* nodePaymentTimer = new QTimer(this);
connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer);
const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000;
nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS);
}
return true;
}
void DomainServer::setupAutomaticNetworking() {
auto nodeList = DependencyManager::get<LimitedNodeList>();
@ -467,9 +434,9 @@ void DomainServer::setupAutomaticNetworking() {
setupICEHeartbeatForFullNetworking();
}
if (!didSetupAccountManagerWithAccessToken()) {
qDebug() << "Cannot send heartbeat to data server without an access token.";
qDebug() << "Add an access token to your config file or via the web interface.";
if (!resetAccountManagerAccessToken()) {
qDebug() << "Will not send heartbeat to Metaverse API without an access token.";
qDebug() << "If this is not a temporary domain add an access token to your config file or via the web interface.";
return;
}
@ -526,6 +493,19 @@ void DomainServer::setupICEHeartbeatForFullNetworking() {
// we need this DS to know what our public IP is - start trying to figure that out now
limitedNodeList->startSTUNPublicSocketUpdate();
// to send ICE heartbeats we'd better have a private key locally with an uploaded public key
auto& accountManager = AccountManager::getInstance();
auto domainID = accountManager.getAccountInfo().getDomainID();
// if we have an access token and we don't have a private key or the current domain ID has changed
// we should generate a new keypair
if (!accountManager.getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) {
accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID());
}
// hookup to the signal from account manager that tells us when keypair is available
connect(&accountManager, &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange);
if (!_iceHeartbeatTimer) {
// setup a timer to heartbeat with the ice-server every so often
_iceHeartbeatTimer = new QTimer { this };
@ -1082,11 +1062,76 @@ void DomainServer::sendHeartbeatToDataServer(const QString& networkAddress) {
domainUpdateJSON.toUtf8());
}
// TODO: have data-web respond with ice-server hostname to use
void DomainServer::sendHeartbeatToIceServer() {
if (!_iceServerSocket.getAddress().isNull()) {
DependencyManager::get<LimitedNodeList>()->sendHeartbeatToIceServer(_iceServerSocket);
auto& accountManager = AccountManager::getInstance();
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
if (!accountManager.getAccountInfo().hasPrivateKey()) {
qWarning() << "Cannot send an ice-server heartbeat without a private key for signature.";
qWarning() << "Waiting for keypair generation to complete before sending ICE heartbeat.";
if (!limitedNodeList->getSessionUUID().isNull()) {
accountManager.generateNewDomainKeypair(limitedNodeList->getSessionUUID());
} else {
qWarning() << "Attempting to send ICE server heartbeat with no domain ID. This is not supported";
}
return;
}
// NOTE: I'd love to specify the correct size for the packet here, but it's a little trickey with
// QDataStream and the possibility of IPv6 address for the sockets.
if (!_iceServerHeartbeatPacket) {
_iceServerHeartbeatPacket = NLPacket::create(PacketType::ICEServerHeartbeat);
}
bool shouldRecreatePacket = false;
if (_iceServerHeartbeatPacket->getPayloadSize() > 0) {
// if either of our sockets have changed we need to re-sign the heartbeat
// first read the sockets out from the current packet
_iceServerHeartbeatPacket->seek(0);
QDataStream heartbeatStream(_iceServerHeartbeatPacket.get());
QUuid senderUUID;
HifiSockAddr publicSocket, localSocket;
heartbeatStream >> senderUUID >> publicSocket >> localSocket;
if (senderUUID != limitedNodeList->getSessionUUID()
|| publicSocket != limitedNodeList->getPublicSockAddr()
|| localSocket != limitedNodeList->getLocalSockAddr()) {
shouldRecreatePacket = true;
}
} else {
shouldRecreatePacket = true;
}
if (shouldRecreatePacket) {
// either we don't have a heartbeat packet yet or some combination of sockets, ID and keypair have changed
// and we need to make a new one
// reset the position in the packet before writing
_iceServerHeartbeatPacket->reset();
// write our plaintext data to the packet
QDataStream heartbeatDataStream(_iceServerHeartbeatPacket.get());
heartbeatDataStream << limitedNodeList->getSessionUUID()
<< limitedNodeList->getPublicSockAddr() << limitedNodeList->getLocalSockAddr();
// setup a QByteArray that points to the plaintext data
auto plaintext = QByteArray::fromRawData(_iceServerHeartbeatPacket->getPayload(), _iceServerHeartbeatPacket->getPayloadSize());
// generate a signature for the plaintext data in the packet
auto signature = accountManager.getAccountInfo().signPlaintext(plaintext);
// pack the signature with the data
heartbeatDataStream << signature;
}
// send the heartbeat packet to the ice server now
limitedNodeList->sendUnreliablePacket(*_iceServerHeartbeatPacket, _iceServerSocket);
}
}
@ -1970,3 +2015,31 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMes
});
}
}
void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message) {
static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3;
static int numHeartbeatDenials = 0;
if (++numHeartbeatDenials > NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN) {
qDebug() << "Received" << NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN << "heartbeat denials from ice-server"
<< "- re-generating keypair now";
// we've hit our threshold of heartbeat denials, trigger a keypair re-generation
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
AccountManager::getInstance().generateNewDomainKeypair(limitedNodeList->getSessionUUID());
// reset our number of heartbeat denials
numHeartbeatDenials = 0;
}
}
void DomainServer::handleKeypairChange() {
if (_iceServerHeartbeatPacket) {
// reset the payload size of the ice-server heartbeat packet - this causes the packet to be re-generated
// the next time we go to send an ice-server heartbeat
_iceServerHeartbeatPacket->setPayloadSize(0);
// send a heartbeat to the ice server immediately
sendHeartbeatToIceServer();
}
}

View file

@ -61,6 +61,7 @@ public slots:
void processNodeJSONStatsPacket(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer sendingNode);
void processPathQueryPacket(QSharedPointer<ReceivedMessage> packet);
void processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
private slots:
void aboutToQuit();
@ -78,16 +79,16 @@ private slots:
void handleTempDomainError(QNetworkReply& requestReply);
void queuedQuit(QString quitMessage, int exitCode);
void handleKeypairChange();
private:
void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid());
bool optionallySetupOAuth();
bool optionallyReadX509KeyAndCertificate();
bool optionallySetupAssignmentPayment();
void optionallyGetTemporaryName(const QStringList& arguments);
bool didSetupAccountManagerWithAccessToken();
bool resetAccountManagerAccessToken();
void setupAutomaticNetworking();
@ -153,6 +154,7 @@ private:
DomainServerSettingsManager _settingsManager;
HifiSockAddr _iceServerSocket;
std::unique_ptr<NLPacket> _iceServerHeartbeatPacket;
QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer

View file

@ -13,13 +13,30 @@
//
// Goes into "paused" when the '.' key (and automatically when started in HMD), and normal when pressing any key.
// See MAIN CONTROL, below, for what "paused" actually does.
var OVERLAY_RATIO = 1920 / 1080;
var OVERLAY_WIDTH = 1920;
var OVERLAY_HEIGHT = 1080;
var OVERLAY_RATIO = OVERLAY_WIDTH / OVERLAY_HEIGHT;
var OVERLAY_DATA = {
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT,
imageURL: "http://hifi-content.s3.amazonaws.com/alan/production/images/images/Overlay-Viz-blank.png",
color: {red: 255, green: 255, blue: 255},
alpha: 1
};
var lastOverlayPosition = { x: 0, y: 0, z: 0};
var OVERLAY_DATA_HMD = {
position: lastOverlayPosition,
width: OVERLAY_WIDTH,
height: OVERLAY_HEIGHT,
url: "http://hifi-content.s3.amazonaws.com/alan/production/images/images/Overlay-Viz-blank.png",
color: {red: 255, green: 255, blue: 255},
alpha: 1,
scale: 2,
isFacingAvatar: true,
drawInFront: true
};
// ANIMATION
// We currently don't have play/stopAnimation integrated with the animation graph, but we can get the same effect
// using an animation graph with a state that we turn on and off through the animation var defined with that state.
@ -64,29 +81,74 @@ function stopAwayAnimation() {
// OVERLAY
var overlay = Overlays.addOverlay("image", OVERLAY_DATA);
var overlayHMD = Overlays.addOverlay("image3d", OVERLAY_DATA_HMD);
function moveCloserToCamera(positionAtHUD) {
// we don't actually want to render at the slerped look at... instead, we want to render
// slightly closer to the camera than that.
var MOVE_CLOSER_TO_CAMERA_BY = -0.25;
var cameraFront = Quat.getFront(Camera.orientation);
var closerToCamera = Vec3.multiply(cameraFront, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera
var slightlyCloserPosition = Vec3.sum(positionAtHUD, closerToCamera);
return slightlyCloserPosition;
}
function showOverlay() {
var properties = {visible: true},
// Update for current screen size, keeping overlay proportions constant.
screen = Controller.getViewportDimensions(),
screenRatio = screen.x / screen.y;
if (screenRatio < OVERLAY_RATIO) {
properties.width = screen.x;
properties.height = screen.x / OVERLAY_RATIO;
properties.x = 0;
properties.y = (screen.y - properties.height) / 2;
var properties = {visible: true};
if (HMD.active) {
// make sure desktop version is hidden
Overlays.editOverlay(overlay, { visible: false });
lastOverlayPosition = HMD.getHUDLookAtPosition3D();
var actualOverlayPositon = moveCloserToCamera(lastOverlayPosition);
Overlays.editOverlay(overlayHMD, { visible: true, position: actualOverlayPositon });
} else {
properties.height = screen.y;
properties.width = screen.y * OVERLAY_RATIO;
properties.y = 0;
properties.x = (screen.x - properties.width) / 2;
// make sure HMD is hidden
Overlays.editOverlay(overlayHMD, { visible: false });
// Update for current screen size, keeping overlay proportions constant.
var screen = Controller.getViewportDimensions();
// keep the overlay it's natural size and always center it...
Overlays.editOverlay(overlay, { visible: true,
x: ((screen.x - OVERLAY_WIDTH) / 2),
y: ((screen.y - OVERLAY_HEIGHT) / 2) });
}
Overlays.editOverlay(overlay, properties);
}
function hideOverlay() {
Overlays.editOverlay(overlay, {visible: false});
Overlays.editOverlay(overlayHMD, {visible: false});
}
hideOverlay();
function maybeMoveOverlay() {
if (isAway) {
// if we switched from HMD to Desktop, make sure to hide our HUD overlay and show the
// desktop overlay
if (!HMD.active) {
showOverlay(); // this will also recenter appropriately
}
if (HMD.active) {
// Note: instead of moving it directly to the lookAt, we will move it slightly toward the
// new look at. This will result in a more subtle slerp toward the look at and reduce jerkiness
var EASE_BY_RATIO = 0.1;
var lookAt = HMD.getHUDLookAtPosition3D();
var lookAtChange = Vec3.subtract(lookAt, lastOverlayPosition);
var halfWayBetweenOldAndLookAt = Vec3.multiply(lookAtChange, EASE_BY_RATIO);
var newOverlayPosition = Vec3.sum(lastOverlayPosition, halfWayBetweenOldAndLookAt);
lastOverlayPosition = newOverlayPosition;
var actualOverlayPositon = moveCloserToCamera(lastOverlayPosition);
Overlays.editOverlay(overlayHMD, { visible: true, position: actualOverlayPositon });
// make sure desktop version is hidden
Overlays.editOverlay(overlay, { visible: false });
}
}
}
// MAIN CONTROL
var wasMuted, isAway;
@ -106,6 +168,12 @@ function goAway() {
MyAvatar.setEnableMeshVisible(false); // just for our own display, without changing point of view
playAwayAnimation(); // animation is still seen by others
showOverlay();
// tell the Reticle, we want to stop capturing the mouse until we come back
Reticle.allowMouseCapture = false;
if (HMD.active) {
Reticle.visible = false;
}
}
function goActive() {
if (!isAway) {
@ -119,20 +187,28 @@ function goActive() {
MyAvatar.setEnableMeshVisible(true); // IWBNI we respected Developer->Avatar->Draw Mesh setting.
stopAwayAnimation();
hideOverlay();
// tell the Reticle, we are ready to capture the mouse again and it should be visible
Reticle.allowMouseCapture = true;
Reticle.visible = true;
if (HMD.active) {
Reticle.position = HMD.getHUDLookAtPosition2D();
}
}
function maybeGoActive(event) {
if (event.isAutoRepeat) { // isAutoRepeat is true when held down (or when Windows feels like it)
return;
}
if (!isAway && (event.text === '.')) {
if (!isAway && (event.text == 'ESC')) {
goAway();
} else {
goActive();
}
}
var wasHmdActive = false;
var wasMouseCaptured = false;
var wasHmdActive = HMD.active;
var wasMouseCaptured = Reticle.mouseCaptured;
function maybeGoAway() {
if (HMD.active !== wasHmdActive) {
wasHmdActive = !wasHmdActive;
@ -141,10 +217,8 @@ function maybeGoAway() {
}
}
// If the mouse has gone from captured, to non-captured state,
// then it likely means the person is still in the HMD, but has
// tabbed away from the application (meaning they don't have mouse
// control) and they likely want to go into an away state
// If the mouse has gone from captured, to non-captured state, then it likely means the person is still in the HMD, but
// tabbed away from the application (meaning they don't have mouse control) and they likely want to go into an away state
if (Reticle.mouseCaptured !== wasMouseCaptured) {
wasMouseCaptured = !wasMouseCaptured;
if (!wasMouseCaptured) {
@ -153,6 +227,8 @@ function maybeGoAway() {
}
}
Script.update.connect(maybeMoveOverlay);
Script.update.connect(maybeGoAway);
Controller.mousePressEvent.connect(goActive);
Controller.keyPressEvent.connect(maybeGoActive);

View file

@ -1,3 +1,4 @@
"use strict";
// handControllerGrab.js
//
// Created by Eric Levin on 9/2/15
@ -801,6 +802,8 @@ function MyController(hand) {
this.isInitialGrab = false;
this.doubleParentGrab = false;
this.checkForStrayChildren();
if (this.state == STATE_SEARCHING ? this.triggerSmoothedReleased() : this.bumperReleased()) {
this.setState(STATE_RELEASE);
return;
@ -1046,9 +1049,14 @@ function MyController(hand) {
}
this.distanceHolding = function() {
var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition;
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation);
// controller pose is in avatar frame
var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand);
// transform it into world frame
var controllerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation));
var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
var now = Date.now();
@ -1056,12 +1064,10 @@ function MyController(hand) {
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectRotation = grabbedProperties.rotation;
this.currentObjectTime = now;
this.handRelativePreviousPosition = Vec3.subtract(handControllerPosition, MyAvatar.position);
this.handPreviousRotation = handRotation;
this.currentCameraOrientation = Camera.orientation;
// compute a constant based on the initial conditions which we use below to exagerate hand motion onto the held object
this.radiusScalar = Math.log(Vec3.distance(this.currentObjectPosition, handControllerPosition) + 1.0);
this.radiusScalar = Math.log(Vec3.distance(this.currentObjectPosition, controllerPosition) + 1.0);
if (this.radiusScalar < 1.0) {
this.radiusScalar = 1.0;
}
@ -1091,10 +1097,10 @@ function MyController(hand) {
this.callEntityMethodOnGrabbed("startDistanceGrab");
}
this.currentAvatarPosition = MyAvatar.position;
this.currentAvatarOrientation = MyAvatar.orientation;
this.turnOffVisualizations();
this.previousControllerPosition = controllerPosition;
this.previousControllerRotation = controllerRotation;
};
this.continueDistanceHolding = function() {
@ -1106,10 +1112,13 @@ function MyController(hand) {
this.heartBeat(this.grabbedEntity);
var handPosition = this.getHandPosition();
var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition;
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(controllerHandInput).rotation);
// controller pose is in avatar frame
var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand);
// transform it into world frame
var controllerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, avatarControllerPose.translation));
var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
@ -1122,66 +1131,27 @@ function MyController(hand) {
return;
}
var now = Date.now();
this.currentObjectTime = now;
// the action was set up on a previous call. update the targets.
var radius = Vec3.distance(this.currentObjectPosition, handControllerPosition) *
var radius = Vec3.distance(this.currentObjectPosition, controllerPosition) *
this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
if (radius < 1.0) {
radius = 1.0;
}
// how far did avatar move this timestep?
var currentPosition = MyAvatar.position;
var avatarDeltaPosition = Vec3.subtract(currentPosition, this.currentAvatarPosition);
this.currentAvatarPosition = currentPosition;
// scale delta controller hand movement by radius.
var handMoved = Vec3.multiply(Vec3.subtract(controllerPosition, this.previousControllerPosition), radius);
// How far did the avatar turn this timestep?
// Note: The following code is too long because we need a Quat.quatBetween() function
// that returns the minimum quaternion between two quaternions.
var currentOrientation = MyAvatar.orientation;
if (Quat.dot(currentOrientation, this.currentAvatarOrientation) < 0.0) {
var negativeCurrentOrientation = {
x: -currentOrientation.x,
y: -currentOrientation.y,
z: -currentOrientation.z,
w: -currentOrientation.w
};
var avatarDeltaOrientation = Quat.multiply(negativeCurrentOrientation, Quat.inverse(this.currentAvatarOrientation));
} else {
var avatarDeltaOrientation = Quat.multiply(currentOrientation, Quat.inverse(this.currentAvatarOrientation));
}
var handToAvatar = Vec3.subtract(handControllerPosition, this.currentAvatarPosition);
var objectToAvatar = Vec3.subtract(this.currentObjectPosition, this.currentAvatarPosition);
var handMovementFromTurning = Vec3.subtract(Quat.multiply(avatarDeltaOrientation, handToAvatar), handToAvatar);
var objectMovementFromTurning = Vec3.subtract(Quat.multiply(avatarDeltaOrientation, objectToAvatar), objectToAvatar);
this.currentAvatarOrientation = currentOrientation;
// double delta controller rotation
var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation,
controllerRotation,
DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR),
Quat.inverse(this.previousControllerRotation));
// how far did hand move this timestep?
var handMoved = Vec3.subtract(handToAvatar, this.handRelativePreviousPosition);
this.handRelativePreviousPosition = handToAvatar;
// magnify the hand movement but not the change from avatar movement & rotation
handMoved = Vec3.subtract(handMoved, handMovementFromTurning);
var superHandMoved = Vec3.multiply(handMoved, radius);
// Move the object by the magnified amount and then by amount from avatar movement & rotation
var newObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved);
newObjectPosition = Vec3.sum(newObjectPosition, avatarDeltaPosition);
newObjectPosition = Vec3.sum(newObjectPosition, objectMovementFromTurning);
var deltaPosition = Vec3.subtract(newObjectPosition, this.currentObjectPosition); // meters
var now = Date.now();
var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds
this.currentObjectPosition = newObjectPosition;
this.currentObjectTime = now;
// this doubles hand rotation
var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation,
handRotation,
DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR),
Quat.inverse(this.handPreviousRotation));
this.handPreviousRotation = handRotation;
// update the currentObject position and rotation.
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
this.callEntityMethodOnGrabbed("continueDistantGrab");
@ -1192,6 +1162,7 @@ function MyController(hand) {
var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData);
var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position);
if (handControllerData.disableMoveWithHead !== true) {
// mix in head motion
if (MOVE_WITH_HEAD) {
@ -1231,6 +1202,7 @@ function MyController(hand) {
}
}
var handPosition = this.getHandPosition();
//visualizations
if (USE_ENTITY_LINES_FOR_MOVING === true) {
@ -1262,6 +1234,9 @@ function MyController(hand) {
} else {
print("continueDistanceHolding -- updateAction failed");
}
this.previousControllerPosition = controllerPosition;
this.previousControllerRotation = controllerRotation;
};
this.setupHoldAction = function() {
@ -1444,6 +1419,13 @@ function MyController(hand) {
this.heartBeat(this.grabbedEntity);
var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position"]);
if (!props.position) {
// server may have reset, taking our equipped entity with it. move back to "off" stte
this.setState(STATE_RELEASE);
this.callEntityMethodOnGrabbed("releaseGrab");
return;
}
if (props.parentID == MyAvatar.sessionUUID &&
Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) {
// for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip.
@ -1751,6 +1733,17 @@ function MyController(hand) {
return data;
};
this.checkForStrayChildren = function() {
// sometimes things can get parented to a hand and this script is unaware. Search for such entities and
// unhook them.
var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex);
children.forEach(function(childID) {
print("disconnecting stray child of hand: (" + _this.hand + ") " + childID);
Entities.editEntity(childID, {parentID: NULL_UUID});
});
}
this.deactivateEntity = function(entityID, noVelocity) {
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
if (data && data["refCount"]) {

View file

@ -1,5 +1,5 @@
//
// reticleHandRotationTest.js
// handControllerMouse.js
// examples/controllers
//
// Created by Brad Hefta-Gaub on 2015/12/15
@ -10,6 +10,9 @@
//
var DEBUGGING = false;
var angularVelocityTrailingAverage = 0.0; // Global trailing average used to decide whether to move reticle at all
var lastX = 0;
var lastY = 0;
Math.clamp=function(a,b,c) {
return Math.max(b,Math.min(c,a));
@ -31,12 +34,17 @@ function moveReticleAbsolute(x, y) {
var MAPPING_NAME = "com.highfidelity.testing.reticleWithHandRotation";
var mapping = Controller.newMapping(MAPPING_NAME);
mapping.from(Controller.Hardware.Hydra.L3).peek().to(Controller.Actions.ReticleClick);
mapping.from(Controller.Hardware.Hydra.R4).peek().to(Controller.Actions.ReticleClick);
if (Controller.Hardware.Hydra !== undefined) {
mapping.from(Controller.Hardware.Hydra.L3).peek().to(Controller.Actions.ReticleClick);
mapping.from(Controller.Hardware.Hydra.R4).peek().to(Controller.Actions.ReticleClick);
}
if (Controller.Hardware.Vive !== undefined) {
mapping.from(Controller.Hardware.Vive.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick);
mapping.from(Controller.Hardware.Vive.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick);
}
mapping.enable();
function debugPrint(message) {
if (DEBUGGING) {
print(message);
@ -76,7 +84,7 @@ Script.update.connect(function(deltaTime) {
}
// Velocity filter the hand rotation used to position reticle so that it is easier to target small things with the hand controllers
var VELOCITY_FILTER_GAIN = 1.0;
var VELOCITY_FILTER_GAIN = 0.5;
filteredRotatedLeft = Vec3.mix(filteredRotatedLeft, rotatedLeft, Math.clamp(Vec3.length(poseLeft.angularVelocity) * VELOCITY_FILTER_GAIN, 0.0, 1.0));
filteredRotatedRight = Vec3.mix(filteredRotatedRight, rotatedRight, Math.clamp(Vec3.length(poseRight.angularVelocity) * VELOCITY_FILTER_GAIN, 0.0, 1.0));
var rotated = Vec3.mix(filteredRotatedLeft, filteredRotatedRight, leftRightBias);
@ -96,11 +104,17 @@ Script.update.connect(function(deltaTime) {
var y = screenSizeY * yRatio;
// don't move the reticle with the hand controllers unless the controllers are actually being moved
var MINIMUM_CONTROLLER_ANGULAR_VELOCITY = 0.0001;
// take a time average of angular velocity, and don't move mouse at all if it's below threshold
var AVERAGING_INTERVAL = 0.95;
var MINIMUM_CONTROLLER_ANGULAR_VELOCITY = 0.03;
var angularVelocityMagnitude = Vec3.length(poseLeft.angularVelocity) * (1.0 - leftRightBias) + Vec3.length(poseRight.angularVelocity) * leftRightBias;
angularVelocityTrailingAverage = angularVelocityTrailingAverage * AVERAGING_INTERVAL + angularVelocityMagnitude * (1.0 - AVERAGING_INTERVAL);
if (!(xRatio == 0.5 && yRatio == 0) && (angularVelocityMagnitude > MINIMUM_CONTROLLER_ANGULAR_VELOCITY)) {
if (!(xRatio == 0.5 && yRatio == 0) && (angularVelocityTrailingAverage > MINIMUM_CONTROLLER_ANGULAR_VELOCITY) && ((x != lastX) || (y != lastY))) {
moveReticleAbsolute(x, y);
lastX = x;
lastY = y;
}
});

View file

@ -22,3 +22,4 @@ Script.load("grab.js");
Script.load("directory.js");
Script.load("dialTone.js");
Script.load("attachedEntitiesManager.js");
Script.load("depthReticle.js");

View file

@ -5,6 +5,8 @@
// Copyright 2016 High Fidelity, Inc.
//
// When used in HMD, this script will make the reticle depth track to any clickable item in view.
// This script also handles auto-hiding the reticle after inactivity, as well as having the reticle
// seek the look at position upon waking up.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -12,17 +14,104 @@
var APPARENT_2D_OVERLAY_DEPTH = 1.0;
var APPARENT_MAXIMUM_DEPTH = 100.0; // this is a depth at which things all seem sufficiently distant
var lastDepthCheckTime = 0;
var lastDepthCheckTime = Date.now();
var desiredDepth = APPARENT_2D_OVERLAY_DEPTH;
var TIME_BETWEEN_DEPTH_CHECKS = 100;
var MINIMUM_DEPTH_ADJUST = 0.01;
var NON_LINEAR_DIVISOR = 2;
var MINIMUM_SEEK_DISTANCE = 0.01;
Script.update.connect(function(deltaTime) {
var TIME_BETWEEN_DEPTH_CHECKS = 100;
var timeSinceLastDepthCheck = Date.now() - lastDepthCheckTime;
var lastMouseMove = Date.now();
var lastMouseX = Reticle.position.x;
var lastMouseY = Reticle.position.y;
var HIDE_STATIC_MOUSE_AFTER = 3000; // 3 seconds
var shouldSeekToLookAt = false;
var fastMouseMoves = 0;
var averageMouseVelocity = 0;
var WEIGHTING = 1/20; // simple moving average over last 20 samples
var ONE_MINUS_WEIGHTING = 1 - WEIGHTING;
var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 50;
Controller.mouseMoveEvent.connect(function(mouseEvent) {
var now = Date.now();
// if the reticle is hidden, and we're not in away mode...
if (!Reticle.visible && Reticle.allowMouseCapture) {
Reticle.visible = true;
if (HMD.active) {
shouldSeekToLookAt = true;
}
} else {
// even if the reticle is visible, if we're in HMD mode, and the person is moving their mouse quickly (shaking it)
// then they are probably looking for it, and we should move into seekToLookAt mode
if (HMD.active && !shouldSeekToLookAt && Reticle.allowMouseCapture) {
var dx = Reticle.position.x - lastMouseX;
var dy = Reticle.position.y - lastMouseY;
var dt = Math.max(1, (now - lastMouseMove)); // mSecs since last mouse move
var mouseMoveDistance = Math.sqrt((dx*dx) + (dy*dy));
var mouseVelocity = mouseMoveDistance / dt;
averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * mouseVelocity);
if (averageMouseVelocity > AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO) {
shouldSeekToLookAt = true;
}
}
}
lastMouseMove = now;
lastMouseX = mouseEvent.x;
lastMouseY = mouseEvent.y;
});
function seekToLookAt() {
// if we're currently seeking the lookAt move the mouse toward the lookat
if (shouldSeekToLookAt) {
averageMouseVelocity = 0; // reset this, these never count for movement...
var lookAt2D = HMD.getHUDLookAtPosition2D();
var currentReticlePosition = Reticle.position;
var distanceBetweenX = lookAt2D.x - Reticle.position.x;
var distanceBetweenY = lookAt2D.y - Reticle.position.y;
var moveX = distanceBetweenX / NON_LINEAR_DIVISOR;
var moveY = distanceBetweenY / NON_LINEAR_DIVISOR;
var newPosition = { x: Reticle.position.x + moveX, y: Reticle.position.y + moveY };
var closeEnoughX = false;
var closeEnoughY = false;
if (moveX < MINIMUM_SEEK_DISTANCE) {
newPosition.x = lookAt2D.x;
closeEnoughX = true;
}
if (moveY < MINIMUM_SEEK_DISTANCE) {
newPosition.y = lookAt2D.y;
closeEnoughY = true;
}
Reticle.position = newPosition;
if (closeEnoughX && closeEnoughY) {
shouldSeekToLookAt = false;
}
}
}
function autoHideReticle() {
// if we haven't moved in a long period of time, and we're not pointing at some
// system overlay (like a window), then hide the reticle
if (Reticle.visible && !Reticle.pointingAtSystemOverlay) {
var now = Date.now();
var timeSinceLastMouseMove = now - lastMouseMove;
if (timeSinceLastMouseMove > HIDE_STATIC_MOUSE_AFTER) {
Reticle.visible = false;
}
}
}
function checkReticleDepth() {
var now = Date.now();
var timeSinceLastDepthCheck = now - lastDepthCheckTime;
if (timeSinceLastDepthCheck > TIME_BETWEEN_DEPTH_CHECKS) {
var newDesiredDepth = desiredDepth;
lastDepthCheckTime = now;
var reticlePosition = Reticle.position;
// first check the 2D Overlays
if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(reticlePosition)) {
Reticle.setDepth(APPARENT_2D_OVERLAY_DEPTH);
newDesiredDepth = APPARENT_2D_OVERLAY_DEPTH;
} else {
var pickRay = Camera.computePickRay(reticlePosition.x, reticlePosition.y);
@ -37,11 +126,42 @@ Script.update.connect(function(deltaTime) {
// If either the overlays or entities intersect, then set the reticle depth to
// the distance of intersection
if (result.intersects) {
Reticle.setDepth(result.distance);
newDesiredDepth = result.distance;
} else {
// if nothing intersects... set the depth to some sufficiently large depth
Reticle.setDepth(APPARENT_MAXIMUM_DEPTH);
newDesiredDepth = APPARENT_MAXIMUM_DEPTH;
}
}
// If the desired depth has changed, reset our fade start time
if (desiredDepth != newDesiredDepth) {
desiredDepth = newDesiredDepth;
}
}
}
function moveToDesiredDepth() {
// move the reticle toward the desired depth
if (desiredDepth != Reticle.depth) {
// cut distance between desiredDepth and current depth in half until we're close enough
var distanceToAdjustThisCycle = (desiredDepth - Reticle.depth) / NON_LINEAR_DIVISOR;
if (Math.abs(distanceToAdjustThisCycle) < MINIMUM_DEPTH_ADJUST) {
newDepth = desiredDepth;
} else {
newDepth = Reticle.depth + distanceToAdjustThisCycle;
}
Reticle.setDepth(newDepth);
}
}
Script.update.connect(function(deltaTime) {
autoHideReticle(); // auto hide reticle for desktop or HMD mode
if (HMD.active) {
seekToLookAt(); // handle moving the reticle toward the look at
checkReticleDepth(); // make sure reticle is at correct depth
moveToDesiredDepth(); // move the fade the reticle to the desired depth
}
});

View file

@ -89,6 +89,9 @@ var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus";
var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode";
var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode";
// marketplace info, etc. not quite ready yet.
var SHOULD_SHOW_PROPERTY_MENU = false;
var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissions to edit on this domain."
var INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG = "You do not have the necessary permissions to place items on this domain."
@ -711,13 +714,32 @@ var intersection;
var SCALE_FACTOR = 200.0;
function rayPlaneIntersection(pickRay, point, normal) {
function rayPlaneIntersection(pickRay, point, normal) { //
//
// This version of the test returns the intersection of a line with a plane
//
var collides = Vec3.dot(pickRay.direction, normal);
var d = -Vec3.dot(point, normal);
var t = -(Vec3.dot(pickRay.origin, normal) + d) / Vec3.dot(pickRay.direction, normal);
var t = -(Vec3.dot(pickRay.origin, normal) + d) / collides;
return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t));
}
function rayPlaneIntersection2(pickRay, point, normal) {
//
// This version of the test returns false if the ray is directed away from the plane
//
var collides = Vec3.dot(pickRay.direction, normal);
var d = -Vec3.dot(point, normal);
var t = -(Vec3.dot(pickRay.origin, normal) + d) / collides;
if (t < 0.0) {
return false;
} else {
return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t));
}
}
function findClickedEntity(event) {
var pickZones = event.isControl;
@ -758,7 +780,8 @@ function findClickedEntity(event) {
var foundEntity = result.entityID;
return {
pickRay: pickRay,
entityID: foundEntity
entityID: foundEntity,
intersection: result.intersection
};
}
@ -926,6 +949,7 @@ function mouseReleaseEvent(event) {
}
function mouseClickEvent(event) {
var wantDebug = false;
if (isActive && event.isLeftButton) {
var result = findClickedEntity(event);
if (result === null) {
@ -940,11 +964,15 @@ function mouseClickEvent(event) {
var properties = Entities.getEntityProperties(foundEntity);
if (isLocked(properties)) {
print("Model locked " + properties.id);
if (wantDebug) {
print("Model locked " + properties.id);
}
} else {
var halfDiagonal = Vec3.length(properties.dimensions) / 2.0;
print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal);
if (wantDebug) {
print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal);
}
// P P - Model
// /| A - Palm
// / | d B - unit vector toward tip
@ -981,8 +1009,9 @@ function mouseClickEvent(event) {
} else {
selectionManager.addEntity(foundEntity, true);
}
print("Model selected: " + foundEntity);
if (wantDebug) {
print("Model selected: " + foundEntity);
}
selectionDisplay.select(selectedEntityID, event);
if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) {
@ -996,6 +1025,9 @@ function mouseClickEvent(event) {
} else if (event.isRightButton) {
var result = findClickedEntity(event);
if (result) {
if (SHOULD_SHOW_PROPERTY_MENU !== true) {
return;
}
var properties = Entities.getEntityProperties(result.entityID);
if (properties.marketplaceID) {
propertyMenu.marketplaceID = properties.marketplaceID;
@ -1869,9 +1901,11 @@ PopupMenu = function() {
return this;
};
var propertyMenu = PopupMenu();
propertyMenu.onSelectMenuItem = function(name) {
if (propertyMenu.marketplaceID) {
showMarketplace(propertyMenu.marketplaceID);
}

View file

@ -0,0 +1,339 @@
//
// lightningEntity.js
// examples/entityScripts
//
// Created by Brad Hefta-Gaub on 3/1/16.
// Copyright 2016 High Fidelity, Inc.
//
// This is an example of an entity script which will randomly create a flash of lightning and a thunder sound
// effect, as well as a background rain sound effect. It can be applied to any entity, although it works best
// on a zone entity.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt, pointInExtents, vec3equal, setEntityCustomData, getEntityCustomData */
(function () {
"use strict";
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Various configurable settings.
//
// You can change these values to change some of the various effects of the rain storm.
// These values can also be controlled by setting user properties on the entity that you've attached this script to.
// add a "lightning" section to a JSON encoded portion of the user data... for example:
// {
// "lightning": {
// "flashMax": 20,
// "flashMin": 0,
// "flashMaxRandomness": 10,
// "flashIntensityStepRandomeness": 2,
// "averageLighteningStrikeGap": 120,
// "extraRandomRangeLightningStrikeGap": 10,
// "thunderURL": "atp:1336efe995398f5e0d46b37585785de8ba872fe9a9b718264db03748cd41c758.wav",
// "thunderVolume": 0.1,
// "rainURL": "atp:e0cc7438aca776636f6e6f731685781d9999b961c945e4e5760d937be5beecdd.wav",
// "rainVolume": 0.05
// }
// // NOTE: you can have other user data here as well, so long as it's JSON encoded, it won't impact the lightning script
// }
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
var MAX_FLASH_INTENSITY = 20; // this controls how bright the lightning effect appears
var MIN_FLASH_INTENSITY = 0; // this is probably best at 0, but it could be higher, which will make the lightning not fade completely to darkness before going away.
var MAX_FLASH_INTENSITY_RANDOMNESS = 10; // this will add some randomness to the max brightness of the lightning
var FLASH_INTENSITY_STEP_RANDOMNESS = 2; // as the lightning goes from min to max back to min, this will make it more random in it's brightness
var AVERAGE_LIGHTNING_STRIKE_GAP_IN_SECONDS = 120; // how long on average between lighting
var EXTRA_RANDOM_RANGE_LIGHTNING_STRIKE_GAP_IN_SECONDS = 10; // some randomness to the lightning gap
var THUNDER_SOUND_URL = "https://s3.amazonaws.com/hifi-public/brad/rainstorm/thunder-48k.wav"; // thunder sound effect, must be 48k 16bit PCM
var THUNDER_VOLUME = 1; // adjust the volume of the thunder sound effect
var RAIN_SOUND_URL = "https://s3.amazonaws.com/hifi-public/brad/rainstorm/rain.wav"; // the background rain, this will loop
var RAIN_VOLUME = 0.05; // adjust the volume of the rain sound effect.
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Various constants and variables we need access to in all scopes
////////////////////////////////////////////////////////////////////////////////////////////////////////////
var MSECS_PER_SECOND = 1000; // number of milliseconds in a second, a universal truth, don't change this. :)
// This is a little trick for implementing entity scripts. Many of the entity callbacks will have the JavaScript
// "this" variable set, but if you connect to a signal like update, "this" won't correctly point to the instance
// of your entity script, and so we set "_this" and use it in cases we need to access "this". We need to define
// the variable in this scope so that it's not share among all entities.
var _this; // this is important here... or else the _this will be globally scoped.
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Various helper functions...
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helper function for returning either a value, or the default value if the value is undefined. This is
// is handing in parsing JSON where you don't know if the values have been set or not.
function valueOrDefault(value, defaultValue) {
if (value !== undefined) {
return value;
}
return defaultValue;
}
// return a random float between high and low
function randFloat(low, high) {
return low + Math.random() * (high - low);
}
// the "constructor" for our class. pretty simple, it just sets our _this, so we can access it later.
function Lightning() {
_this = this;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// The class prototype
//
// This is the class definition/prototype for our class. Funtions declared here will be accessible
// via the instance of the entity
//
Lightning.prototype = {
// preload()
// This is called by every viewer/interface instance that "sees" the entity you've attached this script to.
// If this is a zone entity, then it will surely be called whenever someone enters the zone.
preload: function (entityID) {
// we always make a point of remember our entityID, so that we can access our entity later
_this.entityID = entityID;
// set up some of our time related state
var now = Date.now();
_this.lastUpdate = now;
_this.lastStrike = now;
// some of our other state related items
_this.lightningID = false; // this will be the entityID for any lightning that we create
_this.lightningActive = false; // are we actively managing lightning
// Get the entities userData property, to see if someone has overridden any of our default settings
var userDataText = Entities.getEntityProperties(entityID, ["userData"]).userData;
var userData = {};
if (userDataText !== "") {
userData = JSON.parse(userDataText);
}
var lightningUserData = valueOrDefault(userData.lightning, {});
_this.flashIntensityStepRandomeness = valueOrDefault(lightningUserData.flashIntensityStepRandomness, FLASH_INTENSITY_STEP_RANDOMNESS);
_this.flashMax = valueOrDefault(lightningUserData.flashMax, MAX_FLASH_INTENSITY);
_this.flashMin = valueOrDefault(lightningUserData.flashMin, MIN_FLASH_INTENSITY);
_this.flashMaxRandomness = valueOrDefault(lightningUserData.flashMaxRandomness, MAX_FLASH_INTENSITY_RANDOMNESS);
_this.averageLightningStrikeGap = valueOrDefault(lightningUserData.averageLightningStrikeGap, AVERAGE_LIGHTNING_STRIKE_GAP_IN_SECONDS);
_this.extraRandomRangeLightningStrikeGap = valueOrDefault(lightningUserData.extraRandomRangeLightningStrikeGap, EXTRA_RANDOM_RANGE_LIGHTNING_STRIKE_GAP_IN_SECONDS);
var thunderURL = valueOrDefault(lightningUserData.thunderURL, THUNDER_SOUND_URL);
_this.thunderSound = SoundCache.getSound(thunderURL); // start downloading the thunder into the cache in case we need it later
_this.thunderVolume = valueOrDefault(lightningUserData.thunderVolume, THUNDER_VOLUME);
var rainURL = valueOrDefault(lightningUserData.rainURL, RAIN_SOUND_URL);
_this.rainSound = SoundCache.getSound(rainURL); // start downloading the rain, we will be using it for sure
_this.rainVolume = valueOrDefault(lightningUserData.rainVolume, RAIN_VOLUME);
_this.rainPlaying = false;
Script.update.connect(_this.onUpdate); // connect our onUpdate to a regular update signal from the interface
},
// unload()
// This is called by every viewer/interface instance that "sees" the entity when you "stop knowing about" the
// entity. Usually this means the user has left then domain, or maybe the entity has been deleted. We need to
// clean up anything transient that we created here. In this case it means disconnecting from the update signal
// and stopping the rain sound if we started it.
unload: function ( /*entityID*/ ) {
Script.update.disconnect(_this.onUpdate);
if (_this.rainInjector !== undefined) {
_this.rainInjector.stop();
}
},
// plays the rain sound effect. This is played locally, which means it doesn't provide spatialization, which
// we don't really want for the ambient sound effect of rain. Also, since it's playing locally, we don't send
// the stream to the audio mixer. Another subtle side effect of playing locally is that the sound isn't in sync
// for all users in the domain, but that's ok for a sound effect like rain which is more of a white noise like
// sound effect.
playLocalRain: function () {
var myPosition = Entities.getEntityProperties(_this.entityID, "position").position;
_this.rainInjector = Audio.playSound(_this.rainSound, {
position: myPosition,
volume: _this.rainVolume,
loop: true,
localOnly: true
});
_this.rainPlaying = true;
},
// this handles a single "step" of the lightning flash effect. It assumes a light entity has already been created,
// and all it really does is change the intensity of the light based on the settings that are part of this entity's
// userData.
flashLightning: function (lightningID) {
var lightningProperties = Entities.getEntityProperties(lightningID, ["userData", "intensity"]);
var lightningParameters = JSON.parse(lightningProperties.userData);
var currentIntensity = lightningProperties.intensity;
var flashDirection = lightningParameters.flashDirection;
var flashMax = lightningParameters.flashMax;
var flashMin = lightningParameters.flashMin;
var flashIntensityStepRandomeness = lightningParameters.flashIntensityStepRandomeness;
var newIntensity = currentIntensity + flashDirection + randFloat(-flashIntensityStepRandomeness, flashIntensityStepRandomeness);
if (flashDirection > 0) {
if (newIntensity >= flashMax) {
flashDirection = -1; // reverse flash
newIntensity = flashMax;
}
} else {
if (newIntensity <= flashMin) {
flashDirection = 1; // reverse flash
newIntensity = flashMin;
}
}
// if we reached 0 intensity, then we're done with this strike...
if (newIntensity === 0) {
_this.lightningActive = false;
Entities.deleteEntity(lightningID);
}
// FIXME - we probably don't need to re-edit the userData of the light... we're only
// changing direction, the rest are the same... we could just store direction in our
// own local variable state
var newLightningParameters = JSON.stringify({
flashDirection: flashDirection,
flashIntensityStepRandomeness: flashIntensityStepRandomeness,
flashMax: flashMax,
flashMin: flashMin
});
// this is what actually creates the effect, changing the intensity of the light
Entities.editEntity(lightningID, {intensity: newIntensity, userData: newLightningParameters});
},
// findMyLightning() is designed to make the script more robust. Since we're an open editable platform
// it's possible that from the time that we started the lightning effect until "now" when we're attempting
// to change the light, some other client might have deleted our light. Before we proceed in editing
// the light, we check to see if it exists.
findMyLightning: function () {
if (_this.lightningID !== false) {
var lightningName = Entities.getEntityProperties(_this.lightningID, "name").name;
if (lightningName !== undefined) {
return _this.lightningID;
}
}
return false;
},
// findOtherLightning() is designed to allow this script to work in a "multi-user" environment, which we
// must assume we are in. Since every user/viewer/client that connect to the domain and "sees" our entity
// is going to run this script, any of them could be in charge of flashing the lightning. So before we
// start to flash the lightning, we will first check to see if someone else already is.
//
// returns true if some other lightning exists... likely because some other viewer is flashing it
// returns false if no other lightning exists...
findOtherLightning: function () {
var myPosition = Entities.getEntityProperties(_this.entityID, "position").position;
// find all entities near me...
var entities = Entities.findEntities(myPosition, 1000);
var checkEntityID;
var checkProperties;
var entity;
for (entity = 0; entity < entities.length; entity++) {
checkEntityID = entities[entity];
checkProperties = Entities.getEntityProperties(checkEntityID, ["name", "type"]);
// check to see if they are lightning
if (checkProperties.type === "Light" && checkProperties.name === "lightning for creator:" + _this.entityID) {
return true;
}
}
return false;
},
// createNewLightning() actually creates new lightning and plays the thunder sound
createNewLightning: function () {
var myPosition = Entities.getEntityProperties(_this.entityID, "position").position;
_this.lightningID = Entities.addEntity({
type: "Light",
name: "lightning for creator:" + _this.entityID,
userData: JSON.stringify({
flashDirection: 1,
flashIntensityStepRandomeness: _this.flashIntensityStepRandomeness,
flashMax: _this.flashMax + randFloat(-_this.flashMaxRandomness, _this.flashMaxRandomness),
flashMin: _this.flashMin
}),
falloffRadius: 1000,
intensity: 0,
position: myPosition,
collisionless: true,
dimensions: {x: 1000, y: 1000, z: 1000},
color: {red: 255, green: 255, blue: 255},
lifetime: 10 // lightning only lasts 10 seconds....
});
// play the thunder...
Audio.playSound(_this.thunderSound, {
position: myPosition,
volume: _this.thunderVolume
});
return _this.lightningID;
},
// onUpdate() this will be called regularly, approximately every frame of the simulation. We will use
// it to determine if we need to do a lightning/thunder strike
onUpdate: function () {
var now = Date.now();
// check to see if rain is downloaded and not yet playing, if so start playing
if (!_this.rainPlaying && _this.rainSound.downloaded) {
_this.playLocalRain();
}
// NOTE: _this.lightningActive will only be TRUE if we are the one who created
// the lightning and we are in charge of flashing it...
if (_this.lightningActive) {
var lightningID = _this.findMyLightning();
// if for some reason our lightning is gone... then just return to non-active state
if (lightningID === false) {
_this.lightningActive = false;
_this.lightningID = false;
} else {
// otherwise, flash our lightning...
_this.flashLightning(lightningID);
}
} else {
// whether or not it's time for us to strike, we always keep an eye out for anyone else
// striking... and if we see someone else striking, we will reset our lastStrike time
if (_this.findOtherLightning()) {
_this.lastStrike = now;
}
var sinceLastStrike = now - _this.lastStrike;
var nextRandomStrikeTime = _this.averageLightningStrikeGap
+ randFloat(-_this.extraRandomRangeLightningStrikeGap,
_this.extraRandomRangeLightningStrikeGap);
if (sinceLastStrike > nextRandomStrikeTime * MSECS_PER_SECOND) {
// so it's time for a strike... let's see if someone else has lightning...
// if no one else is flashing lightning... then we create it...
if (_this.findOtherLightning()) {
_this.lightningActive = false;
_this.lightningID = false;
} else {
_this.createNewLightning();
_this.lightningActive = true;
}
_this.lastStrike = now;
}
}
}
};
return new Lightning();
});

View file

@ -17,8 +17,8 @@ var SIZE = 10.0;
var SEPARATION = 20.0;
var ROWS_X = 30;
var ROWS_Z = 30;
var TYPE = "Sphere"; // Right now this can be "Box" or "Model" or "Sphere"
var MODEL_URL = "https://hifi-public.s3.amazonaws.com/models/props/LowPolyIsland/CypressTreeGroup.fbx";
var TYPE = "Model"; // Right now this can be "Box" or "Model" or "Sphere"
var MODEL_URL = "http://hifi-content.s3.amazonaws.com/DomainContent/CellScience/Instances/vesicle.fbx";
var MODEL_DIMENSION = { x: 33, y: 16, z: 49 };
var RATE_PER_SECOND = 1000; // The entity server will drop data if we create things too fast.
var SCRIPT_INTERVAL = 100;
@ -38,6 +38,7 @@ Script.setInterval(function () {
var numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0);
for (var i = 0; i < numToCreate; i++) {
var position = { x: SIZE + (x * SEPARATION), y: SIZE, z: SIZE + (z * SEPARATION) };
print('position:'+JSON.stringify(position))
if (TYPE == "Model") {
Entities.addEntity({
type: TYPE,

View file

@ -0,0 +1,109 @@
//
// eraserEntityScript.js
// examples/homeContent/eraserEntityScript
//
// Created by Eric Levin on 2/17/15.
// Copyright 2016 High Fidelity, Inc.
//
// This entity script provides logic for an object with attached script to erase nearby marker strokes
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function() {
Script.include("../../libraries/utils.js");
var TRIGGER_CONTROLS = [
Controller.Standard.LT,
Controller.Standard.RT,
];
var _this;
Eraser = function() {
_this = this;
_this.ERASER_TRIGGER_THRESHOLD = 0.2;
_this.STROKE_NAME = "hifi-marker-stroke";
_this.ERASER_TO_STROKE_SEARCH_RADIUS = 0.7;
_this.ERASER_RESET_WAIT_TIME = 3000;
};
Eraser.prototype = {
startEquip: function(id, params) {
_this.equipped = true;
_this.hand = params[0] == "left" ? 0 : 1;
// We really only need to grab position of marker strokes once, and then just check to see if eraser comes near enough to those strokes
Overlays.editOverlay(_this.searchSphere, {
visible: true
});
},
continueEquip: function() {
_this.eraserPosition = Entities.getEntityProperties(_this.entityID, "position").position;
Overlays.editOverlay(_this.searchSphere, {
position: _this.eraserPosition
});
this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[_this.hand]);
if (_this.triggerValue > _this.ERASER_TRIGGER_THRESHOLD) {
_this.continueHolding();
}
},
continueHolding: function() {
var strokeIDs = Entities.findEntities(_this.eraserPosition, _this.ERASER_TO_STROKE_SEARCH_RADIUS);
// Create a map of stroke entities and their positions
strokeIDs.forEach(function(strokeID) {
var strokeProps = Entities.getEntityProperties(strokeID, ["position", "name"]);
if (strokeProps.name === _this.STROKE_NAME && Vec3.distance(_this.eraserPosition, strokeProps.position) < _this.ERASER_TO_STROKE_SEARCH_RADIUS) {
Entities.deleteEntity(strokeID);
}
});
},
releaseEquip: function() {
Overlays.editOverlay(_this.searchSphere, {
visible: false
});
// Once user releases eraser, wait a bit then put marker back to its original position and rotation
Script.setTimeout(function() {
var userData = getEntityUserData(_this.entityID);
Entities.editEntity(_this.entityID, {
position: userData.originalPosition,
rotation: userData.originalRotation,
velocity: {
x: 0,
y: -0.01,
z: 0
}
});
}, _this.ERASER_RESET_WAIT_TIME);
},
preload: function(entityID) {
_this.entityID = entityID;
_this.searchSphere = Overlays.addOverlay('sphere', {
size: _this.ERASER_TO_STROKE_SEARCH_RADIUS,
color: {
red: 200,
green: 10,
blue: 10
},
alpha: 0.2,
solid: true,
visible: false
})
},
unload: function() {
Overlays.deleteOverlay(_this.searchSphere);
}
};
// entity scripts always need to return a newly constructed object of our type
return new Eraser();
});

View file

@ -0,0 +1,219 @@
//
// markerTipEntityScript.js
// examples/homeContent/markerTipEntityScript
//
// Created by Eric Levin on 2/17/15.
// Copyright 2016 High Fidelity, Inc.
//
// This script provides the logic for an object to draw marker strokes on its associated whiteboard
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function() {
Script.include("../../libraries/utils.js");
var TRIGGER_CONTROLS = [
Controller.Standard.LT,
Controller.Standard.RT,
];
var MAX_POINTS_PER_STROKE = 40;
var _this;
MarkerTip = function() {
_this = this;
_this.MARKER_TEXTURE_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/textures/markerStroke.png";
_this.strokeForwardOffset = 0.0001;
_this.STROKE_WIDTH_RANGE = {
min: 0.002,
max: 0.01
};
_this.MAX_MARKER_TO_BOARD_DISTANCE = 1.4;
_this.MIN_DISTANCE_BETWEEN_POINTS = 0.002;
_this.MAX_DISTANCE_BETWEEN_POINTS = 0.1;
_this.strokes = [];
_this.PAINTING_TRIGGER_THRESHOLD = 0.2;
_this.STROKE_NAME = "hifi-marker-stroke";
_this.WHITEBOARD_SURFACE_NAME = "hifi-whiteboardDrawingSurface";
_this.MARKER_RESET_WAIT_TIME = 3000;
};
MarkerTip.prototype = {
startEquip: function(id, params) {
_this.whiteboards = [];
_this.equipped = true;
_this.hand = params[0] == "left" ? 0 : 1;
_this.markerColor = getEntityUserData(_this.entityID).markerColor;
// search for whiteboards
var markerPosition = Entities.getEntityProperties(_this.entityID, "position").position;
var entities = Entities.findEntities(markerPosition, 10);
entities.forEach(function(entity) {
var entityName = Entities.getEntityProperties(entity, "name").name;
if (entityName === _this.WHITEBOARD_SURFACE_NAME) {
_this.whiteboards.push(entity);
}
});
print("intersectable entities " + JSON.stringify(_this.whiteboards))
},
releaseEquip: function() {
_this.resetStroke();
Overlays.editOverlay(_this.laserPointer, {
visible: false
});
// Once user releases marker, wait a bit then put marker back to its original position and rotation
Script.setTimeout(function() {
var userData = getEntityUserData(_this.entityID);
Entities.editEntity(_this.entityID, {
position: userData.originalPosition,
rotation: userData.originalRotation,
velocity: {
x: 0,
y: -0.01,
z: 0
}
});
}, _this.MARKER_RESET_WAIT_TIME);
},
continueEquip: function() {
// cast a ray from marker and see if it hits anything
var markerProps = Entities.getEntityProperties(_this.entityID, ["position", "rotation"]);
var pickRay = {
origin: markerProps.position,
direction: Quat.getFront(markerProps.rotation)
}
var intersection = Entities.findRayIntersectionBlocking(pickRay, true, _this.whiteboards);
if (intersection.intersects && Vec3.distance(intersection.intersection, markerProps.position) < _this.MAX_MARKER_TO_BOARD_DISTANCE) {
_this.currentWhiteboard = intersection.entityID;
var whiteboardRotation = Entities.getEntityProperties(_this.currentWhiteboard, "rotation").rotation;
_this.whiteboardNormal = Quat.getFront(whiteboardRotation);
Overlays.editOverlay(_this.laserPointer, {
visible: true,
position: intersection.intersection,
rotation: whiteboardRotation
})
_this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[_this.hand]);
if (_this.triggerValue > _this.PAINTING_TRIGGER_THRESHOLD) {
_this.paint(intersection.intersection)
} else {
_this.resetStroke();
}
} else {
if (_this.currentStroke) {
_this.resetStroke();
}
Overlays.editOverlay(_this.laserPointer, {
visible: false
});
}
},
newStroke: function(position) {
_this.strokeBasePosition = position;
_this.currentStroke = Entities.addEntity({
type: "PolyLine",
name: _this.STROKE_NAME,
dimensions: {
x: 10,
y: 10,
z: 10
},
position: position,
textures: _this.MARKER_TEXTURE_URL,
color: _this.markerColor,
lifetime: 5000,
});
_this.linePoints = [];
_this.normals = [];
_this.strokes.push(_this.currentStroke);
},
paint: function(position) {
var basePosition = position;
if (!_this.currentStroke) {
if (_this.oldPosition) {
basePosition = _this.oldPosition;
}
_this.newStroke(basePosition);
}
var localPoint = Vec3.subtract(basePosition, _this.strokeBasePosition);
localPoint = Vec3.sum(localPoint, Vec3.multiply(_this.whiteboardNormal, _this.strokeForwardOffset));
if (_this.linePoints.length > 0) {
var distance = Vec3.distance(localPoint, _this.linePoints[_this.linePoints.length - 1]);
if (distance < _this.MIN_DISTANCE_BETWEEN_POINTS) {
return;
}
}
_this.linePoints.push(localPoint);
_this.normals.push(_this.whiteboardNormal);
var strokeWidths = [];
for (var i = 0; i < _this.linePoints.length; i++) {
// Create a temp array of stroke widths for calligraphy effect - start and end should be less wide
var pointsFromCenter = Math.abs(_this.linePoints.length / 2 - i);
var pointWidth = map(pointsFromCenter, 0, this.linePoints.length / 2, _this.STROKE_WIDTH_RANGE.max, this.STROKE_WIDTH_RANGE.min);
strokeWidths.push(pointWidth);
}
Entities.editEntity(_this.currentStroke, {
linePoints: _this.linePoints,
normals: _this.normals,
strokeWidths: strokeWidths
});
if (_this.linePoints.length > MAX_POINTS_PER_STROKE) {
Entities.editEntity(_this.currentStroke, {
parentID: _this.currentWhiteboard
});
_this.currentStroke = null;
_this.oldPosition = position;
}
},
resetStroke: function() {
Entities.editEntity(_this.currentStroke, {
parentID: _this.currentWhiteboard
});
_this.currentStroke = null;
_this.oldPosition = null;
},
preload: function(entityID) {
this.entityID = entityID;
_this.laserPointer = Overlays.addOverlay("circle3d", {
color: {
red: 220,
green: 35,
blue: 53
},
solid: true,
size: 0.01,
});
},
unload: function() {
Overlays.deleteOverlay(_this.laserPointer);
_this.strokes.forEach(function(stroke) {
Entities.deleteEntity(stroke);
});
}
};
// entity scripts always need to return a newly constructed object of our type
return new MarkerTip();
});

View file

@ -0,0 +1,254 @@
//
// whiteboardSpawner.js
// examples/homeContent/whiteboardV2
//
// Created by Eric Levina on 2/17/16
// Copyright 2016 High Fidelity, Inc.
//
// Run this script to spawn a whiteboard, markers, and an eraser.
// To draw on the whiteboard, equip a marker and hold down trigger with marker tip pointed at whiteboard
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
Script.include("../../libraries/utils.js")
var orientation = MyAvatar.orientation;
orientation = Quat.safeEulerAngles(orientation);
var markerRotation = Quat.fromVec3Degrees({
x: orientation.x + 10,
y: orientation.y - 90,
z: orientation.z
})
orientation.x = 0;
var whiteboardRotation = Quat.fromVec3Degrees({
x: 0,
y: orientation.y,
z: 0
});
orientation = Quat.fromVec3Degrees(orientation);
var markers = [];
var whiteboardPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(orientation)));
var WHITEBOARD_MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Whiteboard-4.fbx";
var WHITEBOARD_COLLISION_HULL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/whiteboardCollisionHull.obj";
var whiteboard = Entities.addEntity({
type: "Model",
name: "whiteboard",
modelURL: WHITEBOARD_MODEL_URL,
position: whiteboardPosition,
rotation: whiteboardRotation,
shapeType: 'compound',
compoundShapeURL: WHITEBOARD_COLLISION_HULL_URL,
dimensions: {
x: 1.86,
y: 2.7,
z: 0.4636
},
});
var whiteboardSurfacePosition = Vec3.sum(whiteboardPosition, {
x: 0.0,
y: 0.45,
z: 0.0
});
whiteboardSurfacePosition = Vec3.sum(whiteboardSurfacePosition, Vec3.multiply(-0.02, Quat.getRight(whiteboardRotation)));
var moveForwardDistance = 0.02;
whiteboardFrontSurfacePosition = Vec3.sum(whiteboardSurfacePosition, Vec3.multiply(-moveForwardDistance, Quat.getFront(whiteboardRotation)));
var whiteboardSurfaceSettings = {
type: "Box",
name: "hifi-whiteboardDrawingSurface",
dimensions: {
x: 1.82,
y: 1.8,
z: 0.01
},
color: {
red: 200,
green: 10,
blue: 200
},
position: whiteboardFrontSurfacePosition,
rotation: whiteboardRotation,
visible: false,
parentID: whiteboard
}
var whiteboardFrontDrawingSurface = Entities.addEntity(whiteboardSurfaceSettings);
whiteboardBackSurfacePosition = Vec3.sum(whiteboardSurfacePosition, Vec3.multiply(moveForwardDistance, Quat.getFront(whiteboardRotation)));
whiteboardSurfaceSettings.position = whiteboardBackSurfacePosition;
var whiteboardBackDrawingSurface = Entities.addEntity(whiteboardSurfaceSettings);
var WHITEBOARD_RACK_DEPTH = 1.9;
var ERASER_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/eraser-2.fbx";
var ERASER_SCRIPT_URL = Script.resolvePath("eraserEntityScript.js?v43");
var eraserPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(WHITEBOARD_RACK_DEPTH, Quat.getFront(whiteboardRotation)));
eraserPosition = Vec3.sum(eraserPosition, Vec3.multiply(-0.5, Quat.getRight(whiteboardRotation)));
var eraserRotation = markerRotation;
var eraser = Entities.addEntity({
type: "Model",
modelURL: ERASER_MODEL_URL,
position: eraserPosition,
script: ERASER_SCRIPT_URL,
shapeType: "box",
dimensions: {
x: 0.0858,
y: 0.0393,
z: 0.2083
},
rotation: eraserRotation,
dynamic: true,
gravity: {
x: 0,
y: -1,
z: 0
},
velocity: {
x: 0,
y: -0.1,
z: 0
},
userData: JSON.stringify({
originalPosition: eraserPosition,
originalRotation: eraserRotation,
wearable: {
joints: {
RightHand: [{
x: 0.020,
y: 0.120,
z: 0.049
}, {
x: 0.1004,
y: 0.6424,
z: 0.717,
w: 0.250
}],
LeftHand: [{
x: -0.005,
y: 0.1101,
z: 0.053
}, {
x: 0.723,
y: 0.289,
z: 0.142,
w: 0.610
}]
}
}
})
});
createMarkers();
function createMarkers() {
var modelURLS = [
"https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/marker-blue.fbx",
"https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/marker-red.fbx",
"https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/marker-black.fbx",
];
var markerPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(WHITEBOARD_RACK_DEPTH, Quat.getFront(orientation)));
createMarker(modelURLS[0], markerPosition, {
red: 10,
green: 10,
blue: 200
});
markerPosition = Vec3.sum(markerPosition, Vec3.multiply(-0.2, Quat.getFront(markerRotation)));
createMarker(modelURLS[1], markerPosition, {
red: 200,
green: 10,
blue: 10
});
markerPosition = Vec3.sum(markerPosition, Vec3.multiply(0.4, Quat.getFront(markerRotation)));
createMarker(modelURLS[2], markerPosition, {
red: 10,
green: 10,
blue: 10
});
}
function createMarker(modelURL, markerPosition, markerColor) {
var MARKER_SCRIPT_URL = Script.resolvePath("markerEntityScript.js?v1" + Math.random());
var marker = Entities.addEntity({
type: "Model",
modelURL: modelURL,
rotation: markerRotation,
shapeType: "box",
name: "marker",
dynamic: true,
gravity: {
x: 0,
y: -1,
z: 0
},
velocity: {
x: 0,
y: -0.1,
z: 0
},
position: markerPosition,
dimensions: {
x: 0.027,
y: 0.027,
z: 0.164
},
name: "marker",
script: MARKER_SCRIPT_URL,
userData: JSON.stringify({
originalPosition: markerPosition,
originalRotation: markerRotation,
markerColor: markerColor,
wearable: {
joints: {
RightHand: [{
x: 0.001,
y: 0.139,
z: 0.050
}, {
x: -0.73,
y: -0.043,
z: -0.108,
w: -0.666
}],
LeftHand: [{
x: 0.007,
y: 0.151,
z: 0.061
}, {
x: -0.417,
y: 0.631,
z: -0.389,
w: -0.525
}]
}
}
})
});
markers.push(marker);
}
function cleanup() {
Entities.deleteEntity(whiteboard);
Entities.deleteEntity(whiteboardFrontDrawingSurface);
Entities.deleteEntity(whiteboardBackDrawingSurface);
Entities.deleteEntity(eraser);
markers.forEach(function(marker) {
Entities.deleteEntity(marker);
});
}
Script.scriptEnding.connect(cleanup);

View file

@ -16,6 +16,11 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
SPACE_LOCAL = "local";
SPACE_WORLD = "world";
function objectTranslationPlanePoint(position, dimensions) {
var newPosition = { x: position.x, y: position.y, z: position.z };
newPosition.y -= dimensions.y / 2.0;
return newPosition;
}
SelectionManager = (function() {
var that = {};
@ -2252,15 +2257,20 @@ SelectionDisplay = (function() {
var constrainMajorOnly = false;
var startPosition = null;
var duplicatedEntityIDs = null;
var translateXZTool = {
mode: 'TRANSLATE_XZ',
pickPlanePosition: { x: 0, y: 0, z: 0 },
greatestDimension: 0.0,
startingDistance: 0.0,
startingElevation: 0.0,
onBegin: function(event) {
SelectionManager.saveProperties();
startPosition = SelectionManager.worldPosition;
var dimensions = SelectionManager.worldDimensions;
var pickRay = Camera.computePickRay(event.x, event.y);
initialXZPick = rayPlaneIntersection(pickRay, startPosition, {
initialXZPick = rayPlaneIntersection(pickRay, translateXZTool.pickPlanePosition, {
x: 0,
y: 1,
z: 0
@ -2297,16 +2307,56 @@ SelectionDisplay = (function() {
visible: false
});
},
elevation: function(origin, intersection) {
return (origin.y - intersection.y) / Vec3.distance(origin, intersection);
},
onMove: function(event) {
var wantDebug = false;
pickRay = Camera.computePickRay(event.x, event.y);
var pick = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, {
var pick = rayPlaneIntersection2(pickRay, translateXZTool.pickPlanePosition, {
x: 0,
y: 1,
z: 0
});
// If the pick ray doesn't hit the pick plane in this direction, do nothing.
// this will happen when someone drags across the horizon from the side they started on.
if (!pick) {
if (wantDebug) {
print("Pick ray does not intersect XZ plane.");
}
return;
}
var vector = Vec3.subtract(pick, initialXZPick);
// If the mouse is too close to the horizon of the pick plane, stop moving
var MIN_ELEVATION = 0.02; // largest dimension of object divided by distance to it
var elevation = translateXZTool.elevation(pickRay.origin, pick);
if (wantDebug) {
print("Start Elevation: " + translateXZTool.startingElevation + ", elevation: " + elevation);
}
if ((translateXZTool.startingElevation > 0.0 && elevation < MIN_ELEVATION) ||
(translateXZTool.startingElevation < 0.0 && elevation > -MIN_ELEVATION)) {
if (wantDebug) {
print("too close to horizon!");
}
return;
}
// If the angular size of the object is too small, stop moving
var MIN_ANGULAR_SIZE = 0.01; // Radians
if (translateXZTool.greatestDimension > 0) {
var angularSize = Math.atan(translateXZTool.greatestDimension / Vec3.distance(pickRay.origin, pick));
if (wantDebug) {
print("Angular size = " + angularSize);
}
if (angularSize < MIN_ANGULAR_SIZE) {
return;
}
}
// If shifted, constrain to one axis
if (event.isShifted) {
if (Math.abs(vector.x) > Math.abs(vector.z)) {
@ -2368,7 +2418,7 @@ SelectionDisplay = (function() {
grid.snapToGrid(Vec3.sum(cornerPosition, vector), constrainMajorOnly),
cornerPosition);
var wantDebug = false;
for (var i = 0; i < SelectionManager.selections.length; i++) {
var properties = SelectionManager.savedProperties[SelectionManager.selections[i]];
@ -2645,11 +2695,6 @@ SelectionDisplay = (function() {
pickRayPosition,
planeNormal);
// Overlays.editOverlay(normalLine, {
// start: initialPosition,
// end: Vec3.sum(Vec3.multiply(100000, planeNormal), initialPosition),
// });
SelectionManager.saveProperties();
};
@ -3751,7 +3796,7 @@ SelectionDisplay = (function() {
};
that.mousePressEvent = function(event) {
var wantDebug = false;
if (!event.isLeftButton) {
// if another mouse button than left is pressed ignore it
return false;
@ -3777,7 +3822,7 @@ SelectionDisplay = (function() {
if (result.intersects) {
var wantDebug = false;
if (wantDebug) {
print("something intersects... ");
print(" result.overlayID:" + result.overlayID + "[" + overlayNames[result.overlayID] + "]");
@ -3874,7 +3919,10 @@ SelectionDisplay = (function() {
if (!somethingClicked) {
print("rotate handle case...");
if (wantDebug) {
print("rotate handle case...");
}
// After testing our stretch handles, then check out rotate handles
Overlays.editOverlay(yawHandle, {
@ -3942,15 +3990,17 @@ SelectionDisplay = (function() {
break;
default:
print("mousePressEvent()...... " + overlayNames[result.overlayID]);
if (wantDebug) {
print("mousePressEvent()...... " + overlayNames[result.overlayID]);
}
mode = "UNKNOWN";
break;
}
}
print(" somethingClicked:" + somethingClicked);
print(" mode:" + mode);
if (wantDebug) {
print(" somethingClicked:" + somethingClicked);
print(" mode:" + mode);
}
if (somethingClicked) {
@ -4093,12 +4143,25 @@ SelectionDisplay = (function() {
switch (result.overlayID) {
case selectionBox:
activeTool = translateXZTool;
translateXZTool.pickPlanePosition = result.intersection;
translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y),
SelectionManager.worldDimensions.z);
if (wantDebug) {
print("longest dimension: " + translateXZTool.greatestDimension);
translateXZTool.startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position);
print("starting distance: " + translateXZTool.startingDistance);
translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition);
print(" starting elevation: " + translateXZTool.startingElevation);
}
mode = translateXZTool.mode;
activeTool.onBegin(event);
somethingClicked = true;
break;
default:
print("mousePressEvent()...... " + overlayNames[result.overlayID]);
if (wantDebug) {
print("mousePressEvent()...... " + overlayNames[result.overlayID]);
}
mode = "UNKNOWN";
break;
}

View file

@ -0,0 +1,125 @@
// Created by inigo quilez - iq/2013
// Turbulence and Day/Night cycle added by Michael Olson - OMGparticles/2015
// rain effect adapted from Rainy London by David Hoskins. - https://www.shadertoy.com/view/XdSGDc
// License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
#line 6
const float PI = 3.14159;
uniform float rotationSpeed = 0.005;
uniform float gridLevel = 0.0;
uniform float constellationLevel = 0.0;
uniform float constellationBoundaryLevel = 0.0;
// Axial tilt
const float axialTilt = 0.408407;
const vec2 atsc = vec2(sin(axialTilt), cos(axialTilt));
const mat3 axialTiltMatrix = mat3(
vec3(atsc.y, -atsc.x, 0.0),
vec3(atsc.x, atsc.y, 0.0),
vec3(0.0, 0.0, 1.0));
vec2 directionToSpherical(in vec3 d) {
return vec2(atan(d.x, -d.z), asin(d.y)) * -1.0f;
}
vec2 directionToUv(in vec3 d) {
vec2 uv = directionToSpherical(d);
uv /= PI;
uv.x /= 2.0;
uv += 0.5;
return uv;
}
vec3 stars3(in vec3 d) {
//return rd;
vec3 dir = d * axialTiltMatrix;
float theta = rotationSpeed * iGlobalTime * 4.0;
vec2 sc = vec2(sin(theta), cos(theta));
dir = dir * mat3( vec3(sc.y, 0.0, sc.x),
vec3(0.0, 1.0, 0.0),
vec3(-sc.x, 0.0, sc.y));
vec2 uv = directionToUv(dir);
vec3 starColor = texture2D( iChannel0, uv).xyz;
starColor = pow(starColor, vec3(0.75));
if (gridLevel > 0.0) {
starColor += texture2D( iChannel1, uv).xyz * gridLevel;
}
return starColor;
}
//const vec3 vNightColor = vec3(.15, 0.3, 0.6);
uniform vec3 uDayColor = vec3(0.9,0.7,0.7);
const vec3 vNightColor = vec3(1.0, 1.0, 1.0);
const vec3 vHorizonColor = vec3(0.6, 0.3, 0.4);
const vec3 vSunColor = vec3(1.0,0.8,0.6);
const vec3 vSunRimColor = vec3(1.0,0.66,0.33);
vec4 render( in vec3 ro, in vec3 rd )
{
float fSunSpeed = (rotationSpeed * 10.0 * iGlobalTime) + PI / 2.0 * 3.0;
vec3 sundir = normalize( vec3(cos(fSunSpeed),sin(fSunSpeed),0.0) );
sundir = sundir * axialTiltMatrix;
float sun = clamp( dot(sundir,rd), 0.0, 1.0 );
float fSunHeight = sundir.y;
// below this height will be full night color
float fNightHeight = -0.8;
// above this height will be full day color
float fDayHeight = 0.3;
float fHorizonLength = fDayHeight - fNightHeight;
float fInverseHL = 1.0 / fHorizonLength;
float fHalfHorizonLength = fHorizonLength / 2.0;
float fInverseHHL = 1.0 / fHalfHorizonLength;
float fMidPoint = fNightHeight + fHalfHorizonLength;
float fNightContrib = clamp((fSunHeight - fMidPoint) * (-fInverseHHL), 0.0, 1.0);
float fHorizonContrib = -clamp(abs((fSunHeight - fMidPoint) * (-fInverseHHL)), 0.0, 1.0) + 1.0;
float fDayContrib = clamp((fSunHeight - fMidPoint) * ( fInverseHHL), 0.0, 1.0);
// sky color
vec3 vSkyColor = vec3(0.0);
vSkyColor += mix(vec3(0.0), vNightColor, fNightContrib); // Night
vSkyColor += mix(vec3(0.0), vHorizonColor, fHorizonContrib); // Horizon
vSkyColor += mix(vec3(0.0), uDayColor, fDayContrib); // Day
vec3 col = vSkyColor;
// atmosphere brighter near horizon
col -= clamp(rd.y, 0.0, 0.5);
// draw sun
col += 0.4 * vSunRimColor * pow( sun, 4.0 );
col += 1.0 * vSunColor * pow( sun, 2000.0 );
// stars
float fStarContrib = clamp((fSunHeight - fDayHeight) * (-fInverseHL), 0.0, 1.0);
if (fStarContrib > 0.0) {
vec3 vStarDir = rd;
col = mix(col, stars3(vStarDir), fStarContrib);
}
// Ten layers of rain sheets...
float rainBrightness = 0.15;
vec2 q = vec2(atan(rd.x, -rd.z), asin(rd.y));
float dis = 1;
int sheets = 12;
for (int i = 0; i < sheets; i++) {
float f = pow(dis, .45) + 0.25;
vec2 st = f * (q * vec2(2.0, .05) + vec2(-iGlobalTime*.01 + q.y * .05, iGlobalTime * 0.12));
f = texture2D(iChannel2, st * .5, -59.0).x;
f += texture2D(iChannel2, st*.284, -99.0).y;
f = clamp(pow(abs(f)*.5, 29.0) * 140.0, 0.00, q.y*.4+.05);
vec3 bri = vec3(rainBrightness);
col += bri*f;
dis += 3.5;
}
return vec4(clamp(col, 0.0, 1.0), 1.0);
}
vec3 getSkyboxColor() {
return render( vec3(0.0), normalize(_normal) ).rgb;
}

View file

@ -17,14 +17,14 @@
function ToggleButtonBuddy(x, y, width, height, urls) {
this.up = Overlays.addOverlay("image", {
x: x, y: y, width: width, height: height,
subImage: { x: 0, y: height, width: width, height: height},
subImage: { x: 0, y: 0, width: width, height: height},
imageURL: urls.up,
visible: true,
alpha: 1.0
});
this.down = Overlays.addOverlay("image", {
x: x, y: y, width: width, height: height,
subImage: { x: 0, y: height, width: width, height: height},
subImage: { x: 0, y: 0, width: width, height: height},
imageURL: urls.down,
visible: false,
alpha: 1.0
@ -120,7 +120,6 @@ coatButton.addToggleHandler(function (isDown) {
}
});
function wearAttachment(attachment) {
MyAvatar.attach(attachment.modelURL,
attachment.jointName,
@ -144,5 +143,5 @@ function removeAttachment(attachment) {
Script.scriptEnding.connect(function() {
hatButton.destroy();
coatbutton.destroy();
coatButton.destroy();
});

View file

@ -46,7 +46,7 @@ function emitter(jointName) {
x:0,
y: 0,
z: 0,
w: Math.PI
w: 1
},
emitRadiusStart: 0,
polarStart: 0,
@ -84,7 +84,7 @@ function emitter(jointName) {
alpha: 1,
alphaSpread: 0,
alphaStart: 1,
alphaFinish: 1
alphaFinish: 1
});
return newEmitter;
}

View file

@ -0,0 +1,689 @@
//
// flappyAvatars.js
// examples/toybox
//
// Created by Clement 3/2/16
// Copyright 2015 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() {
// Constants
var TRIGGER_CONTROLS = [
Controller.Standard.LT,
Controller.Standard.RT,
];
var G = 4.0;
Number.prototype.clamp = function(min, max) {
return Math.min(Math.max(this, min), max);
};
var entityManager = new EntityManager();
// Class definitions
function Avatar(DEFAULT_X, DEFAULT_Y, rotation, to3DPosition) {
var DIMENSION = 0.15;
var JUMP_VELOCITY = 1.0;
var xPosition = DEFAULT_X;
var color = { red: 0, green: 0, blue: 255 };
var dimensionsSet = false;
var dimensions = { x: DIMENSION, y: DIMENSION, z: DIMENSION };
var yPosition = DEFAULT_Y;
var yVelocity = 0.0;
var yAcceleration = -G;
var airSwipeSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/Air Swipe 05.wav");
var injector = null;
this.position = function() {
return { x: xPosition, y: yPosition };
}
this.dimensions = function() {
return dimensions;
}
var id = entityManager.add({
type: "Model",
modelURL: MyAvatar.skeletonModelURL,
animation: {
url: "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/fly.fbx",
running: true,
fps: 30,
firstFrame: 1.0,
lastFrame: 80.0,
currentFrame: 1.0,
loop: true,
hold: false
},
position: to3DPosition(this.position()),
rotation: rotation,
dimensions: dimensions
});
this.changeModel = function(modelURL) {
dimensionsSet = false;
dimensions = { x: 0.10, y: 0.10, z: 0.01 };
Entities.editEntity(id, {
modelURL: modelURL,
rotation: Quat.multiply(rotation, Quat.fromPitchYawRollDegrees(0, -90, 0)),
dimensions: dimensions,
animation: {running: false}
});
airSwipeSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/8bit Jump 03.wav");
injector = null;
}
this.jump = function() {
yVelocity = JUMP_VELOCITY;
if (airSwipeSound.downloaded && !injector) {
injector = Audio.playSound(airSwipeSound, { position: to3DPosition(this.position()), volume: 0.05 });
} else if (injector) {
injector.restart();
}
}
this.update = function(deltaTime) {
if (!dimensionsSet) {
var properties = Entities.getEntityProperties(id, ["naturalDimensions"]);
var naturalDimensions = properties.naturalDimensions;
if (naturalDimensions.x != 1 || naturalDimensions.y != 1 || naturalDimensions.z != 1) {
var max = Math.max(naturalDimensions.x, Math.max(naturalDimensions.y, naturalDimensions.z));
dimensions.x = naturalDimensions.x / max * dimensions.x;
dimensions.y = naturalDimensions.y / max * dimensions.y;
dimensions.z = naturalDimensions.z / max * dimensions.z;
dimensionsSet = true;
Entities.editEntity(id, {
dimensions: dimensions
});
}
} else {
dimensions = Entities.getEntityProperties(id, ["dimensions"]).dimensions;
}
yPosition += deltaTime * (yVelocity + deltaTime * yAcceleration / 2.0);
yVelocity += deltaTime * yAcceleration;
}
this.draw = function() {
Entities.editEntity(id, {
position: to3DPosition(this.position())
});
}
this.reset = function() {
yPosition = DEFAULT_Y;
yVelocity = 0.0;
}
}
function Coin(xPosition, yPosition, to3DPosition) {
var velocity = 0.4;
var dimensions = { x: 0.0625, y: 0.0625, z: 0.0088 };
this.position = function() {
return { x: xPosition, y: yPosition };
}
var id = entityManager.add({
type: "Model",
modelURL: "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/coin.fbx",
angularVelocity: { x: 0, y: 20, z: 0 },
position: to3DPosition(this.position()),
dimensions:dimensions
});
this.update = function(deltaTime) {
xPosition -= deltaTime * velocity;
}
this.isColliding = function(avatar) {
var deltaX = Math.abs(this.position().x - avatar.position().x);
var deltaY = Math.abs(this.position().Y - avatar.position().Y);
if (deltaX < (avatar.dimensions().x + dimensions.x) / 2.0 &&
deltaX < (avatar.dimensions().y + dimensions.y) / 2.0) {
return true;
}
return false;
}
this.draw = function() {
Entities.editEntity(id, { position: to3DPosition({ x: xPosition, y: yPosition }) });
}
this.clear = function() {
entityManager.remove(id);
}
}
function Pipe(xPosition, yPosition, height, gap, to3DPosition) {
var velocity = 0.4;
var width = 0.1;
this.position = function() {
return xPosition;
}
var upHeight = yPosition - (height + gap);
var upYPosition = height + gap + upHeight / 2.0;
var idUp = entityManager.add({
type: "Model",
modelURL: "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/greenPipe.fbx",
rotation: Quat.fromPitchYawRollDegrees(180, 0, 0),
position: to3DPosition({ x: xPosition, y: upYPosition }),
dimensions: { x: width, y: upHeight, z: width }
});
var idDown = entityManager.add({
type: "Model",
modelURL: "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/greenPipe.fbx",
position: to3DPosition({ x: xPosition, y: height / 2.0 }),
dimensions: { x: width, y: height, z: width }
});
this.update = function(deltaTime) {
xPosition -= deltaTime * velocity;
}
this.isColliding = function(avatar) {
var deltaX = Math.abs(this.position() - avatar.position().x);
if (deltaX < (avatar.dimensions().z + width) / 2.0) {
var factor = 0.8;
var upDistance = (yPosition - upHeight) - (avatar.position().y + avatar.dimensions().y * factor);
var downDistance = (avatar.position().y - avatar.dimensions().y * factor) - height;
if (upDistance <= 0 || downDistance <= 0) {
return true;
}
}
return false;
}
this.draw = function() {
Entities.editEntity(idUp, { position: to3DPosition({ x: xPosition, y: upYPosition }) });
Entities.editEntity(idDown, { position: to3DPosition({ x: xPosition, y: height / 2.0 }) });
}
this.clear = function() {
entityManager.remove(idUp);
entityManager.remove(idDown);
}
}
function Pipes(newPipesPosition, newPipesHeight, to3DPosition, moveScore) {
var lastPipe = 0;
var pipesInterval = 2.0;
var pipes = new Array();
var coins = new Array();
var coinsSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/Coin.wav");
var injector = null;
this.update = function(deltaTime, gameTime, startedPlaying) {
// Move pipes forward
pipes.forEach(function(element) {
element.update(deltaTime);
});
// Move coins forward
coins.forEach(function(element) {
element.update(deltaTime);
});
// Delete pipes over the end
var count = 0;
while(count < pipes.length && pipes[count].position() <= 0.0) {
pipes[count].clear();
count++;
}
if (count > 0) {
pipes = pipes.splice(count);
}
// Delete coins over the end
count = 0;
while(count < coins.length && coins[count].position() <= 0.0) {
coins[count].clear();
count++;
}
if (count > 0) {
coins = coins.splice(count);
}
// Make new pipes and coins
if (startedPlaying && gameTime - lastPipe > pipesInterval) {
var min = 0.4;
var max = 0.7;
var height = Math.random() * (max - min) + min;
pipes.push(new Pipe(newPipesPosition, newPipesHeight, height, 0.5, to3DPosition));
coins.push(new Coin(newPipesPosition, height + 0.5 / 2.0, to3DPosition));
lastPipe = gameTime;
}
}
this.isColliding = function(avatar) {
// Check coin collection
var collected = -1;
coins.forEach(function(element, index) {
if (element.isColliding(avatar)) {
element.clear();
collected = index;
moveScore(1);
if (coinsSound.downloaded && !injector) {
injector = Audio.playSound(coinsSound, { position: to3DPosition({ x: newPipesPosition, y: newPipesHeight }), volume: 0.1 });
} else if (injector) {
injector.restart();
}
}
});
if (collected > -1) {
coins.splice(collected, 1);
}
// Check collisions
var isColliding = false;
pipes.forEach(function(element) {
isColliding |= element.isColliding(avatar);
});
return isColliding;
}
this.draw = function() {
// Drawing pipes
pipes.forEach(function(element) {
element.draw();
});
// Drawing coins
coins.forEach(function(element) {
element.draw();
});
}
this.clear = function() {
// Clearing pipes
pipes.forEach(function(element) {
element.clear();
});
pipes = new Array();
// Clearing coins
coins.forEach(function(element) {
element.clear();
});
coins = new Array();
}
}
function Score(space, bestScore) {
var score = 0;
var highScore = bestScore;
var topOffset = Vec3.multiplyQbyV(space.orientation, { x: -0.1, y: 0.2, z: -0.2 });
var topLeft = Vec3.sum(space.position, topOffset);
var bottomOffset = Vec3.multiplyQbyV(space.orientation, { x: -0.1, y: 0.0, z: -0.2 });
var bottomLeft = Vec3.sum(space.position, bottomOffset);
var numberDimensions = { x: 0.0660, y: 0.1050, z: 0.0048 };
function numberUrl(number) {
return "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/" + number + ".fbx"
}
function digitPosition(digit) {
return Vec3.multiplyQbyV(space.orientation, { x: 0.3778 + digit * (numberDimensions.x + 0.01), y: 0.0, z: 0.0 });
}
this.score = function() {
return score;
}
this.highScore = function() {
return highScore;
}
var numDigits = 3;
var bestId = entityManager.add({
type: "Model",
modelURL: "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/best.fbx",
position: topLeft,
rotation: Quat.multiply(space.orientation, Quat.fromPitchYawRollDegrees(90, 0, 0)),
dimensions: { x: 0.2781, y: 0.0063, z: 0.1037 }
});
var bestDigitsId = []
for (var i = 0; i < numDigits; i++) {
bestDigitsId[i] = entityManager.add({
type: "Model",
modelURL: numberUrl(0),
position: Vec3.sum(topLeft, digitPosition(i)),
rotation: space.orientation,
dimensions: numberDimensions
});
}
var scoreId = entityManager.add({
type: "Model",
modelURL: "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/score.fbx",
position: bottomLeft,
rotation: Quat.multiply(space.orientation, Quat.fromPitchYawRollDegrees(90, 0, 0)),
dimensions: { x: 0.3678, y: 0.0063, z: 0.1037 }
});
var scoreDigitsId = []
for (var i = 0; i < numDigits; i++) {
scoreDigitsId[i] = entityManager.add({
type: "Model",
modelURL: numberUrl(0),
position: Vec3.sum(bottomLeft, digitPosition(i)),
rotation: space.orientation,
dimensions: numberDimensions
});
}
this.moveScore = function(delta) {
score += delta;
if (score > highScore) {
highScore = score;
}
}
this.resetScore = function() {
score = 0;
}
this.draw = function() {
for (var i = 0; i < numDigits; i++) {
Entities.editEntity(bestDigitsId[i], { modelURL: numberUrl(Math.floor((highScore / Math.pow(10, numDigits- i - 1)) % 10)) });
}
for (var i = 0; i < numDigits; i++) {
Entities.editEntity(scoreDigitsId[i], { modelURL: numberUrl(Math.floor(score / Math.pow(10, numDigits - i - 1)) % 10) });
}
}
}
function Game(bestScore) {
// public methods
this.start = function() {
if (!isRunning) {
isRunning = true;
setup();
}
}
this.stop = function() {
if (isRunning) {
cleanup();
isRunning = false;
}
}
// Game loop setup
var timestamp = 0;
this.idle = function(triggerValue) {
var now = Date.now();
var deltaTime = (now - timestamp) / 1000.0;
if (timestamp === 0) {
deltaTime = 0;
}
gameTime += deltaTime;
inputs(triggerValue);
update(deltaTime);
draw();
timestamp = now;
}
// Constants
var spaceDimensions = { x: 2.0, y: 1.5, z: 0.01 };
var spaceDistance = 1.5;
var spaceYOffset = 0.6;
// Private game state
var that = this;
var isRunning = false;
var startedPlaying = false;
var coolDown = 1.5;
var lastLost = -coolDown;
var gameTime = 0;
var isJumping = false;
var lastJumpValue = 0.0;
var lastTriggerValue = 0.0;
var TRIGGER_THRESHOLD = 0.9;
var space = null;
var avatar = null;
var pipes = null;
var score = null;
var gameOverSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/Game Over.wav");
var injector = null;
var directions = ["UP", "DOWN", "LEFT", "RIGHT"];
var sequence = [directions[0], directions[0], directions[1], directions[1], directions[2], directions[3], directions[2], directions[3], "b", "a"];
var current = 0;
function keyPress(event) {
if (event.text === sequence[current]) {
++current;
} else {
current = 0;
}
if (current === sequence.length) {
avatar.changeModel("https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/mario.fbx");
current = 0;
}
}
var isBoardReset = true;
function moveScore(delta) {
score.moveScore(delta);
}
this.score = function() {
return score.score();
}
this.highScore = function() {
return score.highScore();
}
function setup() {
space = {
position: getSpacePosition(),
orientation: getSpaceOrientation(),
dimensions: getSpaceDimensions()
}
var rotation = Quat.multiply(space.orientation, Quat.fromPitchYawRollDegrees(0, 90, 0));
avatar = new Avatar(space.dimensions.x / 2.0, space.dimensions.y / 2.0, rotation, to3DPosition);
pipes = new Pipes(space.dimensions.x, space.dimensions.y, to3DPosition, moveScore);
score = new Score(space, bestScore);
Controller.keyPressEvent.connect(keyPress);
}
function inputs(triggerValue) {
if (!startedPlaying && !isBoardReset && (gameTime - lastLost) > coolDown) {
score.resetScore();
avatar.reset();
pipes.clear();
isBoardReset = true;
}
if (triggerValue > TRIGGER_THRESHOLD &&
lastTriggerValue < TRIGGER_THRESHOLD &&
(gameTime - lastLost) > coolDown) {
isJumping = true;
startedPlaying = true;
}
lastTriggerValue = triggerValue;
}
function update(deltaTime) {
// Keep entities alive
entityManager.update(deltaTime);
if (!startedPlaying && (gameTime - lastLost) < coolDown && !isBoardReset) {
return;
}
// Update Avatar
if (!startedPlaying && avatar.position().y < spaceDimensions.y / 2.0) {
isJumping = true;
}
// Apply jumps
if (isJumping) {
avatar.jump();
isJumping = false;
}
avatar.update(deltaTime);
pipes.update(deltaTime, gameTime, startedPlaying);
// Check lost
var hasLost = avatar.position().y < 0.0 ||
avatar.position().y > space.dimensions.y ||
pipes.isColliding(avatar);
// Cleanup
if (hasLost) {
if (gameOverSound.downloaded && !injector) {
injector = Audio.playSound(gameOverSound, { position: space.position, volume: 0.4 });
} else if (injector) {
injector.restart();
}
isBoardReset = false;
startedPlaying = false;
lastLost = gameTime;
}
}
function draw() {
avatar.draw();
pipes.draw();
score.draw();
}
function cleanup() {
entityManager.removeAll();
Controller.keyPressEvent.disconnect(keyPress);
}
// Private methods
function getSpacePosition() {
var forward = Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.FRONT);
var spacePosition = Vec3.sum(MyAvatar.position, Vec3.multiply(spaceDistance, forward));
return Vec3.sum(spacePosition, Vec3.multiply(spaceYOffset, Vec3.UP));
}
function getSpaceOrientation() {
return MyAvatar.orientation;
}
function getSpaceDimensions() {
return spaceDimensions;
}
function to3DPosition(position) {
var position2D = {
x: position.x - space.dimensions.x / 2.0,
y: position.y - space.dimensions.y / 2.0,
z: 0.0
}
return Vec3.sum(space.position, Vec3.multiplyQbyV(space.orientation, position2D));
}
}
function EntityManager() {
var OBJECTS_LIFETIME = 1;
var entities = new Array();
var lifetime = OBJECTS_LIFETIME;
this.setLifetime = function(newLifetime) {
lifetime = newLifetime;
this.update();
}
this.add = function(properties) {
// Add to scene
properties.lifetime = lifetime;
var entityID = Entities.addEntity(properties);
// Add to array
entities.push({ id: entityID, properties: properties });
return entityID;
}
this.update = function(deltaTime) {
entities.forEach(function(element) {
// Get entity's age
var properties = Entities.getEntityProperties(element.id, ["age"]);
// Update entity's lifetime
Entities.editEntity(element.id, { lifetime: properties.age + lifetime });
});
}
this.remove = function(entityID) {
// Remove from scene
Entities.deleteEntity(entityID);
// Remove from array
entities = entities.filter(function(element) {
return element.id !== entityID;
});
}
this.removeAll = function() {
// Remove all from scene
entities.forEach(function(element) {
Entities.deleteEntity(element.id);
});
// Remove all from array
entities = new Array();
}
}
PartableGame = function() {
this.entityID = null;
this.equipped = false;
this.triggerValue = 0.0;
this.hand = 0;
this.game = null;
};
PartableGame.prototype = {
preload: function(entityID) {
this.entityID = entityID;
},
unload: function() {
},
startEquip: function(id, params) {
this.equipped = true;
this.hand = params[0] == "left" ? 0 : 1;
var bestScore = 0;
var properties = Entities.getEntityProperties(this.entityID, ["userData"]);
var userData = JSON.parse(properties.userData);
if (userData.highScore) {
bestScore = userData.highScore;
}
this.game = new Game(bestScore);
this.game.start();
},
releaseEquip: function(id, params) {
this.equipped = false;
var properties = Entities.getEntityProperties(this.entityID, ["userData"]);
var userData = JSON.parse(properties.userData);
userData.highScore = this.game.highScore();
properties.userData = JSON.stringify(userData);
Entities.editEntity(this.entityID, properties);
this.game.stop();
delete this.game;
},
continueEquip: function(id, params) {
if (!this.equipped) {
return;
}
this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[this.hand]);
this.game.idle(this.triggerValue);
},
};
// entity scripts always need to return a newly constructed object of our type
return new PartableGame();
});

View file

@ -0,0 +1,34 @@
{
"Entities": [
{
"collisionsWillMove": 1,
"created": "2016-03-03T19:00:10Z",
"dimensions": {
"x": 0.11497055739164352,
"y": 0.11497056484222412,
"z": 0.11497056484222412
},
"dynamic": 1,
"id": "{ee5b25e6-aca2-4dc7-9462-51537d89c126}",
"modelURL": "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/cube.fbx",
"queryAACube": {
"scale": 0.5974045991897583,
"x": -5.1575918197631836,
"y": 23.078603744506836,
"z": 16.521066665649414
},
"rotation": {
"w": 0.92288088798522949,
"x": -0.10148775577545166,
"y": -0.13279926776885986,
"z": 0.34688329696655273
},
"script": "https://raw.githubusercontent.com/Atlante45/hifi/feat/hackaton/examples/toybox/flappyAvatars/flappyAvatars.js",
"scriptTimestamp": 1457031937425,
"shapeType": "box",
"type": "Model",
"userData": "{\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.07079616189002991,\"y\":0.20177987217903137,\"z\":0.06374628841876984},{\"x\":-0.5863648653030396,\"y\":-0.46007341146469116,\"z\":0.46949487924575806,\"w\":-0.4733745753765106}],\"LeftHand\":[{\"x\":-0.018704339861869812,\"y\":0.20499876141548157,\"z\":0.08445858210325241},{\"x\":0.2061777561903,\"y\":-0.6629757881164551,\"z\":0.5865269303321838,\"w\":0.41706138849258423}]}},\"grabbableKey\":{\"invertSolidWhileHeld\":true},\"resetMe\":{\"resetMe\":true},\"highScore\":0}"
}
],
"Version": 57
}

View file

@ -104,8 +104,9 @@
green: 255,
blue: 255
},
intensity: 2,
exponent: 0.3,
intensity: 1,
falloffRadius:0.9,
exponent: 0.5,
cutoff: 20,
lifetime: LIFETIME,
position: lightTransform.p,
@ -128,6 +129,8 @@
blue: 255
},
exponent: 0,
intensity:1.0,
falloffRadius:0.3,
lifetime: LIFETIME,
cutoff: 90, // in degrees
position: glowLightTransform.p,

View file

@ -0,0 +1,100 @@
//
// createNewMusicPlayerOnClick.js
//
// Created by Brad Hefta-Gaub on 3/3/16.
// Copyright 2016 High Fidelity, Inc.
//
// Entity Script that you can attach to any entity to have it spawn new "music players"
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
(function(){
var musicPlayerScript = Script.resolvePath("./musicPlayer.js");
var imageShader = Script.resolvePath("./imageShader.fs");
var defaultImage = Script.resolvePath("./defaultImage.jpg");
function getPositionToCreateEntity() {
var distance = 0.5;
var direction = Quat.getFront(Camera.orientation);
var offset = Vec3.multiply(distance, direction);
var placementPosition = Vec3.sum(Camera.position, offset);
var cameraPosition = Camera.position;
var HALF_TREE_SCALE = 16384;
var cameraOutOfBounds = Math.abs(cameraPosition.x) > HALF_TREE_SCALE || Math.abs(cameraPosition.y) > HALF_TREE_SCALE || Math.abs(cameraPosition.z) > HALF_TREE_SCALE;
var placementOutOfBounds = Math.abs(placementPosition.x) > HALF_TREE_SCALE || Math.abs(placementPosition.y) > HALF_TREE_SCALE || Math.abs(placementPosition.z) > HALF_TREE_SCALE;
if (cameraOutOfBounds && placementOutOfBounds) {
return null;
}
placementPosition.x = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.x));
placementPosition.y = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.y));
placementPosition.z = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.z));
return placementPosition;
}
function createNewIPOD() {
var iPodPosition = { x: 0, y: .5, z: 0};
var iPodDimensions = { x: 0.15, y: 0.3, z: 0.03 };
var overlayDimensions = { x: 0.13, y: 0.13, z: 0.001 };
var boxOverlayDimensions = { x: 0.13, y: 0.13, z: 0.0001 };
var iPodID = Entities.addEntity({
type: "Box",
name: "music player",
position: iPodPosition,
dimensions: iPodDimensions,
color: { red: 150, green: 150, blue: 150},
script: musicPlayerScript,
dynamic: true
});
print(iPodID);
var textID = Entities.addEntity({
type: "Text",
name: "now playing",
position: { x: 0, y: (0.5+0.07), z: 0.0222},
dimensions: overlayDimensions,
color: { red: 150, green: 150, blue: 150},
parentID: iPodID,
lineHeight: 0.01,
text: "Pause"
});
var newAlbumArt = JSON.stringify(
{
"ProceduralEntity": {
"version":2,
"shaderUrl":imageShader,
"uniforms":{"iSpeed":0,"iShell":1},
"channels":[defaultImage]
}
});
var albumArtID = Entities.addEntity({
type: "Box",
name: "album art",
position: { x: 0, y: (0.5-0.07), z: 0.01506},
dimensions: boxOverlayDimensions,
color: { red: 255, green: 255, blue: 255},
userData: newAlbumArt,
parentID: iPodID
});
Entities.editEntity(iPodID, { position: getPositionToCreateEntity() });
}
this.clickDownOnEntity = function(myID, mouseEvent) {
createNewIPOD();
};
})

Binary file not shown.

After

(image error) Size: 57 KiB

View file

@ -0,0 +1,44 @@
float aspect(vec2 v) {
return v.x / v.y;
}
vec3 indexedTexture() {
vec2 uv = _position.xy;
uv += 0.5;
uv.y = 1.0 - uv.y;
float targetAspect = iWorldScale.x / iWorldScale.y;
float sourceAspect = aspect(iChannelResolution[0].xy);
float aspectCorrection = sourceAspect / targetAspect;
if (aspectCorrection > 1.0) {
float offset = aspectCorrection - 1.0;
float halfOffset = offset / 2.0;
uv.y -= halfOffset;
uv.y *= aspectCorrection;
} else {
float offset = 1.0 - aspectCorrection;
float halfOffset = offset / 2.0;
uv.x -= halfOffset;
uv.x /= aspectCorrection;
}
if (any(lessThan(uv, vec2(0.0)))) {
return vec3(0.0);
}
if (any(greaterThan(uv, vec2(1.0)))) {
return vec3(0.0);
}
vec4 color = texture(iChannel0, uv);
return color.rgb * max(0.5, sourceAspect) * max(0.9, fract(iWorldPosition.x));
}
float getProceduralColors(inout vec3 diffuse, inout vec3 specular, inout float shininess) {
if (_position.z > -0.49) {
discard;
}
specular = indexedTexture();
return 1.0;
}

View file

@ -0,0 +1,330 @@
//
// musicPlayer.js
//
// Created by Brad Hefta-Gaub on 3/3/16.
// 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
/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt */
(function() {
var imageShader = Script.resolvePath("./imageShader.fs");
var defaultImage = Script.resolvePath("./defaultImage.jpg");
var MAPPING_NAME = "com.highfidelity.musicPlayerEntity";
var PLAYLIST_URL = "https://spreadsheets.google.com/feeds/cells/1x-ceGPGHldkHadARABFWfujLPTOWzXJPhrf2bTwg2cQ/od6/public/basic?alt=json";
var SONG_VOLUME = 0.1;
var HEADPHONES_ATTACHMENT = {
modelURL: "https://s3.amazonaws.com/hifi-public/brad/musicplayer/headphones2-v2.fbx",
jointName: "Head",
translation: {"x": 0, "y": 0.19, "z": 0.06},
rotation: {"x":0,"y":0.7071067690849304,"z":0.7071067690849304,"w":0},
scale: 0.435,
isSoft: false
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Various helper functions...
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helper function for returning either a value, or the default value if the value is undefined. This is
// is handing in parsing JSON where you don't know if the values have been set or not.
function valueOrDefault(value, defaultValue) {
if (value !== undefined) {
return value;
}
return defaultValue;
}
// return a random float between high and low
function randFloat(low, high) {
return low + Math.random() * (high - low);
}
// wears an attachment on MyAvatar
function wearAttachment(attachment) {
MyAvatar.attach(attachment.modelURL,
attachment.jointName,
attachment.translation,
attachment.rotation,
attachment.scale,
attachment.isSoft);
}
// un-wears an attachment from MyAvatar
function removeAttachment(attachment) {
var attachments = MyAvatar.attachmentData;
var i, l = attachments.length;
for (i = 0; i < l; i++) {
if (attachments[i].modelURL === attachment.modelURL) {
attachments.splice(i, 1);
MyAvatar.attachmentData = attachments;
break;
}
}
}
var _this;
MusicPlayer = function() {
_this = this;
this.equipped = false;
};
MusicPlayer.prototype = {
preload: function(entityID) {
print("preload");
//print("rotation:" + JSON.stringify(Quat.fromPitchYawRollDegrees(-90,180,0)));
this.entityID = entityID;
// Get the entities userData property, to see if someone has overridden any of our default settings
var userDataText = Entities.getEntityProperties(entityID, ["userData"]).userData;
//print(userDataText);
var userData = {};
if (userDataText !== "") {
//print("calling JSON.parse");
userData = JSON.parse(userDataText);
//print("userData:" + JSON.stringify(userData));
}
var musicPlayerUserData = valueOrDefault(userData.musicPlayer, {});
this.headphonesAttachment = valueOrDefault(musicPlayerUserData.headphonesAttachment, HEADPHONES_ATTACHMENT);
this.track = 0; // start at the first track
this.playlistURL = valueOrDefault(musicPlayerUserData.playlistURL, PLAYLIST_URL);
this.songVolume = valueOrDefault(musicPlayerUserData.songVolume, SONG_VOLUME);
this.songPlaying = false;
this.loadPlayList();
// Find my screen and any controlls
var children = Entities.getChildrenIDsOfJoint(entityID, 65535);
for (var child in children) {
var childID = children[child];
var childProperties = Entities.getEntityProperties(childID,["type", "name"]);
if (childProperties.type == "Text" && childProperties.name == "now playing") {
this.nowPlayingID = childID;
}
if (childProperties.type == "Box" && childProperties.name == "album art") {
this.albumArt = childID;
}
}
},
unload: function() {
print("unload");
if (_this.songInjector !== undefined) {
_this.songInjector.stop();
}
},
loadPlayList: function() {
print("loadPlayList");
var req = new XMLHttpRequest();
req.open("GET", _this.playlistURL, false);
req.send();
var entries = JSON.parse(req.responseText).feed.entry;
for (entry in entries) {
var cellDetails = entries[entry];
var cellName = cellDetails.title.$t;
var column = Number(cellName.charCodeAt(0)) - Number("A".charCodeAt(0));
var row = Number(cellName.slice(1)) - 1;
var cellContent = cellDetails.content.$t;
//print(JSON.stringify(cellDetails));
//print("["+column +"/"+ row +":"+cellContent+"]");
if (_this.playList === undefined) {
_this.playList = new Array();
}
if (_this.playList[row] === undefined) {
_this.playList[row] = { };
}
switch (column) {
case 0:
_this.playList[row].title = cellContent;
break;
case 1:
_this.playList[row].artist = cellContent;
break;
case 2:
_this.playList[row].album = cellContent;
break;
case 3:
_this.playList[row].url = cellContent;
_this.playList[row].sound = SoundCache.getSound(cellContent);
break;
case 4:
_this.playList[row].albumArtURL = cellContent;
break;
}
}
//print(req.responseText);
print(JSON.stringify(_this.playList));
},
startEquip: function(id, params) {
var whichHand = params[0]; // "left" or "right"
print("I am equipped in the " + whichHand + " hand....");
this.equipped = true;
this.hand = whichHand;
this.loadPlayList(); // reload the playlist in case...
this.mapHandButtons(whichHand);
wearAttachment(HEADPHONES_ATTACHMENT);
},
continueEquip: function(id, params) {
if (!this.equipped) {
return;
}
},
releaseEquip: function(id, params) {
print("I am NO LONGER equipped....");
this.hand = null;
this.equipped = false;
Controller.disableMapping(MAPPING_NAME);
removeAttachment(HEADPHONES_ATTACHMENT);
this.pause();
},
mapHandButtons: function(hand) {
var mapping = Controller.newMapping(MAPPING_NAME);
if (hand === "left") {
mapping.from(Controller.Standard.LS).peek().to(this.playOrPause);
mapping.from(Controller.Standard.LX).peek().to(this.seek);
mapping.from(Controller.Standard.LY).peek().to(this.volume);
} else {
mapping.from(Controller.Standard.RS).peek().to(this.playOrPause);
mapping.from(Controller.Standard.RX).peek().to(this.seek);
mapping.from(Controller.Standard.RY).peek().to(this.volume);
}
Controller.enableMapping(MAPPING_NAME);
},
playOrPause: function(value) {
print("[playOrPause: "+value+"]");
if (value === 1) {
if (!_this.songPlaying) {
_this.play();
} else {
_this.pause();
}
}
},
play: function() {
print("play current track:" + _this.track);
if (!_this.playList[_this.track].sound.downloaded) {
print("still waiting on track to download....");
return; // not yet ready
}
var statusText = "Song:" + _this.playList[_this.track].title + "\n" +
"Artist:" + _this.playList[_this.track].artist + "\n" +
"Album:" + _this.playList[_this.track].album;
Entities.editEntity(_this.nowPlayingID, { text: statusText });
var newAlbumArt = JSON.stringify(
{
"ProceduralEntity": {
"version":2,
"shaderUrl":imageShader,
"uniforms":{"iSpeed":0,"iShell":1},
"channels":[_this.playList[_this.track].albumArtURL]
}
});
Entities.editEntity(_this.albumArt, { userData: newAlbumArt });
_this.songInjector = Audio.playSound(_this.playList[_this.track].sound, {
position: MyAvatar.position,
volume: _this.songVolume,
loop: false,
localOnly: true
});
_this.songPlaying = true;
},
pause: function() {
print("pause");
Entities.editEntity(_this.nowPlayingID, { text: "Paused" });
if (_this.songInjector !== undefined) {
_this.songInjector.stop();
}
var newAlbumArt = JSON.stringify(
{
"ProceduralEntity": {
"version":2,
"shaderUrl":imageShader,
"uniforms":{"iSpeed":0,"iShell":1},
"channels":[defaultImage]
}
});
Entities.editEntity(_this.albumArt, { userData: newAlbumArt });
_this.songPlaying = false;
},
seek: function(value) {
print("[seek: " + value + "]");
if (value > 0.9) {
_this.next();
} else if (value < -0.9) {
_this.previous();
}
},
volume: function(value) {
print("adjusting volume disabled because of a bug in audio injectors....");
/*
var scaledValue = value / 20;
_this.songVolume += scaledValue;
print("[volume: " + value + "] new volume:" + _this.songVolume);
if (_this.songInjector !== undefined) {
print("[volume: attempting to set options....");
var newOptions = {
position: MyAvatar.position,
volume: _this.songVolume,
loop: false,
localOnly: true
};
_this.songInjector.options = newOptions;
print("[volume: attempting to set options.... RESULT:" + JSON.stringify(_this.songInjector.options));
}
*/
},
previous: function() {
print("[previous]");
_this.pause();
_this.track--;
if (_this.track < 0) {
_this.track = (_this.playList.length - 1);
}
_this.play();
},
next: function() {
print("[next]");
_this.pause();
_this.track++;
if (_this.track >= _this.playList.length) {
_this.track = 0;
}
_this.play();
},
};
// entity scripts always need to return a newly constructed object of our type
return new MusicPlayer();
});

View file

@ -13,51 +13,61 @@ Script.include("../../libraries/utils.js");
var scriptURL = Script.resolvePath('pingPongGun.js');
var MODEL_URL = 'http://hifi-public.s3.amazonaws.com/models/ping_pong_gun/ping_pong_gun.fbx'
var COLLISION_HULL_URL = 'http://hifi-public.s3.amazonaws.com/models/ping_pong_gun/ping_pong_gun_convex.obj';
var MODEL_URL = 'http://hifi-content.s3.amazonaws.com/alan/dev/Pingpong-Gun-New.fbx'
var COLLISION_HULL_URL = 'http://hifi-content.s3.amazonaws.com/alan/dev/Pingpong-Gun-New.obj';
var COLLISION_SOUND_URL = 'http://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/plastic_impact.L.wav';
var center = Vec3.sum(Vec3.sum(MyAvatar.position, {
x: 0,
y: 0.5,
z: 0
x: 0,
y: 0.5,
z: 0
}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation())));
var pingPongGun = Entities.addEntity({
type: "Model",
modelURL: MODEL_URL,
shapeType: 'compound',
compoundShapeURL: COLLISION_HULL_URL,
script: scriptURL,
position: center,
dimensions: {
x: 0.08,
y: 0.21,
z: 0.47
type: "Model",
modelURL: MODEL_URL,
shapeType: 'compound',
compoundShapeURL: COLLISION_HULL_URL,
script: scriptURL,
position: center,
dimensions: {
x: 0.125,
y: 0.3875,
z: 0.9931
},
dynamic: true,
collisionSoundURL: COLLISION_SOUND_URL,
userData: JSON.stringify({
grabbableKey: {
invertSolidWhileHeld: true
},
dynamic: true,
collisionSoundURL: COLLISION_SOUND_URL,
userData: JSON.stringify({
grabbableKey: {
invertSolidWhileHeld: true
},
wearable:{joints:{RightHand:[{x:0.1177130937576294,
y:0.12922893464565277,
z:0.08307232707738876},
{x:0.4934672713279724,
y:0.3605862259864807,
z:0.6394805908203125,
w:-0.4664038419723511}],
LeftHand:[{x:0.09151676297187805,
y:0.13639454543590546,
z:0.09354984760284424},
{x:-0.19628101587295532,
y:0.6418180465698242,
z:0.2830369472503662,
w:0.6851521730422974}]}}
})
wearable: {
joints: {
RightHand: [{
x: 0.1177130937576294,
y: 0.12922893464565277,
z: 0.08307232707738876
}, {
x: 0.4934672713279724,
y: 0.3605862259864807,
z: 0.6394805908203125,
w: -0.4664038419723511
}],
LeftHand: [{
x: 0.09151676297187805,
y: 0.13639454543590546,
z: 0.09354984760284424
}, {
x: -0.19628101587295532,
y: 0.6418180465698242,
z: 0.2830369472503662,
w: 0.6851521730422974
}]
}
}
})
});
function cleanUp() {
Entities.deleteEntity(pingPongGun);
Entities.deleteEntity(pingPongGun);
}
Script.scriptEnding.connect(cleanUp);

View file

@ -22,8 +22,8 @@
//if the trigger value goes below this value, reload the gun.
var RELOAD_THRESHOLD = 0.95;
var GUN_TIP_FWD_OFFSET =-0.35;
var GUN_TIP_UP_OFFSET = 0.040;
var GUN_TIP_FWD_OFFSET = -0.35;
var GUN_TIP_UP_OFFSET = 0.12;
var GUN_FORCE = 9;
var BALL_RESTITUTION = 0.6;
var BALL_LINEAR_DAMPING = 0.4;
@ -45,12 +45,12 @@
green: 255,
blue: 255
};
var TRIGGER_CONTROLS = [
Controller.Standard.LT,
Controller.Standard.RT,
];
PingPongGun.prototype = {
hand: null,
@ -98,8 +98,8 @@
var properties = {
// type: 'Model',
// modelURL:PING_PONG_BALL_URL,
shapeType:'sphere',
type:'Sphere',
shapeType: 'sphere',
type: 'Sphere',
color: BALL_COLOR,
dimensions: BALL_DIMENSIONS,
damping: BALL_LINEAR_DAMPING,
@ -125,7 +125,7 @@
Audio.playSound(this.SHOOTING_SOUND, audioProperties);
},
getGunTipPosition: function(properties) {
//the tip of the gun is going to be in a different place than the center, so we move in space relative to the model to find that position
var frontVector = Quat.getFront(properties.rotation);
@ -148,4 +148,4 @@
// entity scripts always need to return a newly constructed object of our type
return new PingPongGun();
});
});

View file

@ -0,0 +1,29 @@
//
// fireworksLaunchButtonEntityScript.js
//
// Created by Eric Levin on 3/7/2016
// Copyright 2016 High Fidelity, Inc.
//
// This is the chapter 1 entity script of the fireworks tutorial (https://docs.highfidelity.com/docs/fireworks-scripting-tutorial)
//
// Distributed under the Apache License, Version 2.0.
(function() {
Script.include("../../libraries/utils.js");
var _this;
Fireworks = function() {
_this = this;
};
Fireworks.prototype = {
preload: function(entityID) {
_this.entityID = entityID;
}
};
// entity scripts always need to return a newly constructed object of our type
return new Fireworks();
});

View file

@ -0,0 +1,37 @@
//
// fireworksLaunchButtonSpawner.js
//
// Created by Eric Levin on 3/7/2016
// Copyright 2016 High Fidelity, Inc.
//
// This is the chapter 1 interface script of the fireworks tutorial (https://docs.highfidelity.com/docs/fireworks-scripting-tutorial)
//
// Distributed under the Apache License, Version 2.0.
var orientation = Camera.getOrientation();
orientation = Quat.safeEulerAngles(orientation);
orientation.x = 0;
orientation = Quat.fromVec3Degrees(orientation);
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
var SCRIPT_URL = Script.resolvePath("fireworksLaunchButtonEntityScript.js");
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Launch-Button.fbx";
var launchButton = Entities.addEntity({
type: "Model",
name: "hifi-launch-button",
modelURL: MODEL_URL,
position: center,
dimensions: {
x: 0.98,
y: 1.16,
z: 0.98
},
script: SCRIPT_URL,
})
function cleanup() {
Entities.deleteEntity(launchButton);
}
Script.scriptEnding.connect(cleanup);

View file

@ -0,0 +1,92 @@
//
// fireworksLaunchButtonEntityScript.js
//
// Created by Eric Levin on 3/7/2016
// Copyright 2016 High Fidelity, Inc.
//
// This is the chapter 2 entity script of the fireworks tutorial (https://docs.highfidelity.com/docs/fireworks-scripting-tutorial)
//
// Distributed under the Apache License, Version 2.0.
(function() {
Script.include("../../libraries/utils.js");
var _this;
Fireworks = function() {
_this = this;
_this.launchSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/missle+launch.wav");
};
Fireworks.prototype = {
startNearTrigger: function() {
_this.shootFirework(_this.position);
},
startFarTrigger: function() {
_this.shootFirework(_this.position);
},
clickReleaseOnEntity: function() {
_this.shootFirework(_this.position);
},
shootFirework: function(launchPosition) {
Audio.playSound(_this.launchSound, {
position: launchPosition,
volume: 0.5
});
var smoke = Entities.addEntity({
type: "ParticleEffect",
position: _this.position,
velocity: {x: 0, y: 3, z: 0},
lifespan: 10,
lifetime: 20,
isEmitting: true,
name: "Smoke Trail",
maxParticles: 3000,
emitRate: 80,
emitSpeed: 0,
speedSpread: 0,
polarStart: 0,
polarFinish: 0,
azimuthStart: -3.14,
azimuthFinish: 3.14,
emitAcceleration: {
x: 0,
y: 0.01,
z: 0
},
accelerationSpread: {
x: 0.01,
y: 0,
z: 0.01
},
radiusSpread: 0.03,
particleRadius: 0.3,
radiusStart: 0.06,
radiusFinish: 0.9,
alpha: 0.1,
alphaSpread: 0,
alphaStart: 0.7,
alphaFinish: 0,
textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png",
emitterShouldTrail: true,
});
},
preload: function(entityID) {
_this.entityID = entityID;
_this.position = Entities.getEntityProperties(_this.entityID, "position").position;
}
};
// entity scripts always need to return a newly constructed object of our type
return new Fireworks();
});

View file

@ -0,0 +1,37 @@
//
// fireworksLaunchButtonSpawner.js
//
// Created by Eric Levin on 3/7/2016
// Copyright 2016 High Fidelity, Inc.
//
// This is the chapter 2 interface script of the fireworks tutorial (https://docs.highfidelity.com/docs/fireworks-scripting-tutorial)
//
// Distributed under the Apache License, Version 2.0.
var orientation = Camera.getOrientation();
orientation = Quat.safeEulerAngles(orientation);
orientation.x = 0;
orientation = Quat.fromVec3Degrees(orientation);
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
var SCRIPT_URL = Script.resolvePath("fireworksLaunchButtonEntityScript.js");
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Launch-Button.fbx";
var launchButton = Entities.addEntity({
type: "Model",
name: "hifi-launch-button",
modelURL: MODEL_URL,
position: center,
dimensions: {
x: 0.98,
y: 1.16,
z: 0.98
},
script: SCRIPT_URL,
})
function cleanup() {
Entities.deleteEntity(launchButton);
}
Script.scriptEnding.connect(cleanup);

View file

@ -0,0 +1,164 @@
//
// fireworksLaunchButtonEntityScript.js
//
// Created by Eric Levin on 3/7/2016
// Copyright 2016 High Fidelity, Inc.
//
// This is the chapter 3 entity script of the fireworks tutorial (https://docs.highfidelity.com/docs/fireworks-scripting-tutorial)
//
// Distributed under the Apache License, Version 2.0.
(function() {
Script.include("../../libraries/utils.js");
var _this;
Fireworks = function() {
_this = this;
_this.launchSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/missle+launch.wav");
_this.explosionSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/fireworksExplosion.wav");
_this.TIME_TO_EXPLODE = 3000;
};
Fireworks.prototype = {
startNearTrigger: function() {
_this.shootFirework(_this.position);
},
startFarTrigger: function() {
_this.shootFirework(_this.position);
},
clickReleaseOnEntity: function() {
_this.shootFirework(_this.position);
},
shootFirework: function(launchPosition) {
Audio.playSound(_this.launchSound, {
position: launchPosition,
volume: 0.5
});
var smoke = Entities.addEntity({
type: "ParticleEffect",
position: _this.position,
velocity: {x: 0, y: 3, z: 0},
linearDamping: 0,
lifespan: 10,
lifetime: 20,
isEmitting: true,
name: "Smoke Trail",
maxParticles: 3000,
emitRate: 80,
emitSpeed: 0,
speedSpread: 0,
polarStart: 0,
polarFinish: 0,
azimuthStart: -3.14,
azimuthFinish: 3.14,
emitAcceleration: {
x: 0,
y: 0.01,
z: 0
},
accelerationSpread: {
x: 0.01,
y: 0,
z: 0.01
},
radiusSpread: 0.03,
particleRadius: 0.3,
radiusStart: 0.06,
radiusFinish: 0.9,
alpha: 0.1,
alphaSpread: 0,
alphaStart: 0.7,
alphaFinish: 0,
textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png",
emitterShouldTrail: true,
});
Script.setTimeout(function() {
var explodePosition = Entities.getEntityProperties(smoke, "position").position;
_this.explodeFirework(explodePosition);
}, _this.TIME_TO_EXPLODE);
},
explodeFirework: function(explodePosition) {
Audio.playSound(_this.explosionSound, {
position: explodePosition
});
var firework = Entities.addEntity({
name: "fireworks emitter",
position: explodePosition,
type: "ParticleEffect",
colorStart: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.7
}),
color: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.5
}),
colorFinish: hslToRgb({
h: Math.random(),
s: 0.5,
l: 0.7
}),
maxParticles: 10000,
lifetime: 20,
lifespan: randFloat(1.5, 3),
emitRate: randInt(500, 5000),
emitSpeed: randFloat(0.5, 2),
speedSpread: 0.2,
emitOrientation: Quat.fromPitchYawRollDegrees(randInt(0, 360), randInt(0, 360), randInt(0, 360)),
polarStart: 1,
polarFinish: randFloat(1.2, 3),
azimuthStart: -Math.PI,
azimuthFinish: Math.PI,
emitAcceleration: {
x: 0,
y: randFloat(-1, -0.2),
z: 0
},
accelerationSpread: {
x: Math.random(),
y: 0,
z: Math.random()
},
particleRadius: randFloat(0.001, 0.1),
radiusSpread: Math.random() * 0.1,
radiusStart: randFloat(0.001, 0.1),
radiusFinish: randFloat(0.001, 0.1),
alpha: randFloat(0.8, 1.0),
alphaSpread: randFloat(0.1, 0.2),
alphaStart: randFloat(0.7, 1.0),
alphaFinish: randFloat(0.7, 1.0),
textures: "http://ericrius1.github.io/PlatosCave/assets/star.png",
});
Script.setTimeout(function() {
Entities.editEntity(firework, {
isEmitting: false
});
}, randInt(500, 1000));
},
preload: function(entityID) {
_this.entityID = entityID;
_this.position = Entities.getEntityProperties(_this.entityID, "position").position;
}
};
// entity scripts always need to return a newly constructed object of our type
return new Fireworks();
});

View file

@ -0,0 +1,37 @@
//
// fireworksLaunchButtonSpawner.js
//
// Created by Eric Levin on 3/7/2016
// Copyright 2016 High Fidelity, Inc.
//
// This is the chapter 3 interface script of the fireworks tutorial (https://docs.highfidelity.com/docs/fireworks-scripting-tutorial)
//
// Distributed under the Apache License, Version 2.0.
var orientation = Camera.getOrientation();
orientation = Quat.safeEulerAngles(orientation);
orientation.x = 0;
orientation = Quat.fromVec3Degrees(orientation);
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
var SCRIPT_URL = Script.resolvePath("fireworksLaunchButtonEntityScript.js");
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Launch-Button.fbx";
var launchButton = Entities.addEntity({
type: "Model",
name: "hifi-launch-button",
modelURL: MODEL_URL,
position: center,
dimensions: {
x: 0.98,
y: 1.16,
z: 0.98
},
script: SCRIPT_URL,
})
function cleanup() {
Entities.deleteEntity(launchButton);
}
Script.scriptEnding.connect(cleanup);

View file

@ -0,0 +1,51 @@
//
// ddebugFramBuffer.js
// examples/utilities/tools/render
//
// Sam Gateau created on 2/18/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
//
var DDB = Render.RenderDeferredTask.DebugDeferredBuffer;
oldConfig = DDB.toJSON();
DDB.enabled = true;
// Set up the qml ui
var qml = Script.resolvePath('framebuffer.qml');
var window = new OverlayWindow({
title: 'Framebuffer Debug',
source: qml,
width: 400, height: 400,
});
window.setPosition(25, 50);
window.closed.connect(function() { Script.stop(); });
// Debug buffer sizing
var resizing = false;
Controller.mousePressEvent.connect(function (e) {
if (shouldStartResizing(e.x)) {
resizing = true;
}
});
Controller.mouseReleaseEvent.connect(function() { resizing = false; });
Controller.mouseMoveEvent.connect(function (e) { resizing && setDebugBufferSize(e.x); });
function shouldStartResizing(eventX) {
var x = Math.abs(eventX - Window.innerWidth * (1.0 + DDB.size.x) / 2.0);
var mode = DDB.mode;
return mode !== -1 && x < 20;
}
function setDebugBufferSize(x) {
x = (2.0 * (x / Window.innerWidth) - 1.0); // scale
x = Math.min(Math.max(-1, x), 1); // clamp
DDB.size = { x: x, y: -1, z: 1, w: 1 };
}
Script.scriptEnding.connect(function () { DDB.fromJSON(oldConfig); });

View file

@ -0,0 +1,52 @@
//
// main.qml
// examples/utilities/tools/render
//
// Created by Zach Pomerantz on 2/8/2016
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4
Column {
spacing: 8
Column {
id: debug
property var config: Render.getConfig("DebugDeferredBuffer")
function setDebugMode(mode) {
debug.config.enabled = (mode != -1);
debug.config.mode = mode;
}
Label { text: qsTr("Debug Buffer") }
ExclusiveGroup { id: bufferGroup }
Repeater {
model: [
"Off",
"Depth",
"Albedo",
"Normal",
"Roughness",
"Metallic",
"Emissive",
"Occlusion",
"Lightmap",
"Lighting",
"Shadow",
"Pyramid Depth",
"Ambient Occlusion",
"Custom Shader"
]
RadioButton {
text: qsTr(modelData)
exclusiveGroup: bufferGroup
checked: index == 0
onCheckedChanged: if (checked) debug.setDebugMode(index - 1);
}
}
}
}

View file

@ -0,0 +1,27 @@
{
"Entities": [
{
"backgroundMode": "skybox",
"dimensions": {
"x": 10000,
"y": 10000,
"z": 10000
},
"name": "Rainy Day/Night Cycle",
"rotation": {
"w": 1,
"x": 0,
"y": 0,
"z": 0
},
"script": "https://s3.amazonaws.com/hifi-public/brad/rainstorm/lightningEntity.js",
"shapeType": "box",
"skybox": {
"url": "https://hifi-public.s3.amazonaws.com/images/SkyboxTextures/CloudyDay1.jpg"
},
"type": "Zone",
"userData":"{\n\"ProceduralEntity\":{\n\"version\":2,\n\"shaderUrl\":\"https://s3.amazonaws.com/hifi-public/brad/rainstorm/rainyDayNightSkybox.fs\",\n\"channels\":[\n\"https://hifi-public.s3.amazonaws.com/austin/assets/images/skybox/starmap_8k.jpg\",\n\"https://hifi-public.s3.amazonaws.com/austin/assets/images/skybox/celestial_grid.jpg\",\n\"https://s3.amazonaws.com/hifi-public/brad/rainstorm/noise.jpg\",\n\"https://s3.amazonaws.com/hifi-public/brad/noise.jpg\"\n],\n\"uniforms\":{\n\"rotationSpeed\":0.001,\n\"uDayColor\":[0.4,0.3,0.3],\n\"constellationLevel\":0.0,\n\"constellationBoundaryLevel\":0.00,\n\"gridLevel\":0.0\n}\n},\n\"lightning\":{\n\"flashMax\":20,\n\"flashMin\":0,\n\"flashIntensityStepRandomeness\":2,\n\"flashMaxRandomness\":10,\n\"averageLightningStrikeGap\":120,\n\"extraRandomRangeLightningStrikeGap\":5,\n\"thunderURL\":\"https://s3.amazonaws.com/hifi-public/brad/rainstorm/thunder-48k.wav\",\n\"thunderVolume\":0.1,\n\"rainURL\":\"https://s3.amazonaws.com/hifi-public/brad/rainstorm/rain.wav\",\n\"rainVolume\":0.05\n}\n}"
}
],
"Version": 57
}

View file

@ -6,3 +6,17 @@ setup_hifi_project(Network)
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared)
package_libraries_for_deployment()
# find OpenSSL
find_package(OpenSSL REQUIRED)
if (APPLE AND ${OPENSSL_INCLUDE_DIR} STREQUAL "/usr/include")
# this is a user on OS X using system OpenSSL, which is going to throw warnings since they're deprecating for their common crypto
message(WARNING "The found version of OpenSSL is the OS X system version. This will produce deprecation warnings."
"\nWe recommend you install a newer version (at least 1.0.1h) in a different directory and set OPENSSL_ROOT_DIR in your env so Cmake can find it.")
endif ()
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
# append OpenSSL to our list of libraries to link
target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES})

View file

@ -9,14 +9,21 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QTimer>
#include "IceServer.h"
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <QtCore/QJsonDocument>
#include <QtCore/QTimer>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include <LimitedNodeList.h>
#include <NetworkingConstants.h>
#include <udt/PacketHeaders.h>
#include <SharedUtil.h>
#include "IceServer.h"
const int CLEAR_INACTIVE_PEERS_INTERVAL_MSECS = 1 * 1000;
const int PEER_SILENCE_THRESHOLD_MSECS = 5 * 1000;
@ -45,7 +52,6 @@ IceServer::IceServer(int argc, char* argv[]) :
QTimer* inactivePeerTimer = new QTimer(this);
connect(inactivePeerTimer, &QTimer::timeout, this, &IceServer::clearInactivePeers);
inactivePeerTimer->start(CLEAR_INACTIVE_PEERS_INTERVAL_MSECS);
}
bool IceServer::packetVersionMatch(const udt::Packet& packet) {
@ -70,9 +76,14 @@ void IceServer::processPacket(std::unique_ptr<udt::Packet> packet) {
if (nlPacket->getType() == PacketType::ICEServerHeartbeat) {
SharedNetworkPeer peer = addOrUpdateHeartbeatingPeer(*nlPacket);
// so that we can send packets to the heartbeating peer when we need, we need to activate a socket now
peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr());
if (peer) {
// so that we can send packets to the heartbeating peer when we need, we need to activate a socket now
peer->activateMatchingOrNewSymmetricSocket(nlPacket->getSenderSockAddr());
} else {
// we couldn't verify this peer - respond back to them so they know they may need to perform keypair re-generation
static auto deniedPacket = NLPacket::create(PacketType::ICEServerHeartbeatDenied);
_serverSocket.writePacket(*deniedPacket, nlPacket->getSenderSockAddr());
}
} else if (nlPacket->getType() == PacketType::ICEServerQuery) {
QDataStream heartbeatStream(nlPacket.get());
@ -114,31 +125,135 @@ SharedNetworkPeer IceServer::addOrUpdateHeartbeatingPeer(NLPacket& packet) {
// pull the UUID, public and private sock addrs for this peer
QUuid senderUUID;
HifiSockAddr publicSocket, localSocket;
QByteArray signature;
QDataStream heartbeatStream(&packet);
heartbeatStream >> senderUUID;
heartbeatStream >> publicSocket >> localSocket;
heartbeatStream >> senderUUID >> publicSocket >> localSocket;
// make sure we have this sender in our peer hash
SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID);
auto signedPlaintext = QByteArray::fromRawData(packet.getPayload(), heartbeatStream.device()->pos());
heartbeatStream >> signature;
if (!matchingPeer) {
// if we don't have this sender we need to create them now
matchingPeer = QSharedPointer<NetworkPeer>::create(senderUUID, publicSocket, localSocket);
_activePeers.insert(senderUUID, matchingPeer);
// make sure this is a verified heartbeat before performing any more processing
if (isVerifiedHeartbeat(senderUUID, signedPlaintext, signature)) {
// make sure we have this sender in our peer hash
SharedNetworkPeer matchingPeer = _activePeers.value(senderUUID);
qDebug() << "Added a new network peer" << *matchingPeer;
if (!matchingPeer) {
// if we don't have this sender we need to create them now
matchingPeer = QSharedPointer<NetworkPeer>::create(senderUUID, publicSocket, localSocket);
_activePeers.insert(senderUUID, matchingPeer);
qDebug() << "Added a new network peer" << *matchingPeer;
} else {
// we already had the peer so just potentially update their sockets
matchingPeer->setPublicSocket(publicSocket);
matchingPeer->setLocalSocket(localSocket);
}
// update our last heard microstamp for this network peer to now
matchingPeer->setLastHeardMicrostamp(usecTimestampNow());
return matchingPeer;
} else {
// we already had the peer so just potentially update their sockets
matchingPeer->setPublicSocket(publicSocket);
matchingPeer->setLocalSocket(localSocket);
// not verified, return the empty peer object
return SharedNetworkPeer();
}
}
bool IceServer::isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature) {
// check if we have a private key for this domain ID - if we do not then fire off the request for it
auto it = _domainPublicKeys.find(domainID);
if (it != _domainPublicKeys.end()) {
// attempt to verify the signature for this heartbeat
const unsigned char* publicKeyData = reinterpret_cast<const unsigned char*>(it->second.constData());
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, it->second.size());
if (rsaPublicKey) {
auto hashedPlaintext = QCryptographicHash::hash(plaintext, QCryptographicHash::Sha256);
int verificationResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(hashedPlaintext.constData()),
hashedPlaintext.size(),
reinterpret_cast<const unsigned char*>(signature.constData()),
signature.size(),
rsaPublicKey);
// free up the public key and remove connection token before we return
RSA_free(rsaPublicKey);
if (verificationResult == 1) {
// this is the only success case - we return true here to indicate that the heartbeat is verified
return true;
} else {
qDebug() << "Failed to verify heartbeat for" << domainID << "- re-requesting public key from API.";
}
} else {
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
qWarning() << "Could not convert in-memory public key for" << domainID << "to usable RSA public key.";
qWarning() << "Re-requesting public key from API";
}
}
// update our last heard microstamp for this network peer to now
matchingPeer->setLastHeardMicrostamp(usecTimestampNow());
// we could not verify this heartbeat (missing public key, could not load public key, bad actor)
// ask the metaverse API for the right public key and return false to indicate that this is not verified
requestDomainPublicKey(domainID);
return matchingPeer;
return false;
}
void IceServer::requestDomainPublicKey(const QUuid& domainID) {
// send a request to the metaverse API for the public key for this domain
QNetworkAccessManager* manager = new QNetworkAccessManager { this };
connect(manager, &QNetworkAccessManager::finished, this, &IceServer::publicKeyReplyFinished);
QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL };
QString publicKeyPath = QString("/api/v1/domains/%1/public_key").arg(uuidStringWithoutCurlyBraces(domainID));
publicKeyURL.setPath(publicKeyPath);
QNetworkRequest publicKeyRequest { publicKeyURL };
publicKeyRequest.setAttribute(QNetworkRequest::User, domainID);
qDebug() << "Requesting public key for domain with ID" << domainID;
manager->get(publicKeyRequest);
}
void IceServer::publicKeyReplyFinished(QNetworkReply* reply) {
// get the domain ID from the QNetworkReply attribute
QUuid domainID = reply->request().attribute(QNetworkRequest::User).toUuid();
if (reply->error() == QNetworkReply::NoError) {
// pull out the public key and store it for this domain
// the response should be JSON
QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll());
static const QString DATA_KEY = "data";
static const QString PUBLIC_KEY_KEY = "public_key";
static const QString STATUS_KEY = "status";
static const QString SUCCESS_VALUE = "success";
auto responseObject = responseDocument.object();
if (responseObject[STATUS_KEY].toString() == SUCCESS_VALUE) {
auto dataObject = responseObject[DATA_KEY].toObject();
if (dataObject.contains(PUBLIC_KEY_KEY)) {
_domainPublicKeys[domainID] = QByteArray::fromBase64(dataObject[PUBLIC_KEY_KEY].toString().toUtf8());
} else {
qWarning() << "There was no public key present in response for domain with ID" << domainID;
}
} else {
qWarning() << "The metaverse API did not return success for public key request for domain with ID" << domainID;
}
} else {
// there was a problem getting the public key for the domain
// log it since it will be re-requested on the next heartbeat
qWarning() << "Error retreiving public key for domain with ID" << domainID << "-" << reply->errorString();
}
}
void IceServer::sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr) {

View file

@ -16,13 +16,15 @@
#include <QtCore/QSharedPointer>
#include <QUdpSocket>
#include <UUIDHasher.h>
#include <NetworkPeer.h>
#include <HTTPConnection.h>
#include <HTTPManager.h>
#include <NLPacket.h>
#include <udt/Socket.h>
typedef QHash<QUuid, SharedNetworkPeer> NetworkPeerHash;
class QNetworkReply;
class IceServer : public QCoreApplication, public HTTPRequestHandler {
Q_OBJECT
@ -31,6 +33,7 @@ public:
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false);
private slots:
void clearInactivePeers();
void publicKeyReplyFinished(QNetworkReply* reply);
private:
bool packetVersionMatch(const udt::Packet& packet);
void processPacket(std::unique_ptr<udt::Packet> packet);
@ -38,10 +41,19 @@ private:
SharedNetworkPeer addOrUpdateHeartbeatingPeer(NLPacket& incomingPacket);
void sendPeerInformationPacket(const NetworkPeer& peer, const HifiSockAddr* destinationSockAddr);
bool isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& plaintext, const QByteArray& signature);
void requestDomainPublicKey(const QUuid& domainID);
QUuid _id;
udt::Socket _serverSocket;
using NetworkPeerHash = QHash<QUuid, SharedNetworkPeer>;
NetworkPeerHash _activePeers;
HTTPManager _httpManager;
using DomainPublicKeyHash = std::unordered_map<QUuid, QByteArray>;
DomainPublicKeyHash _domainPublicKeys;
};
#endif // hifi_IceServer_h

View file

@ -268,7 +268,7 @@ if (WIN32)
set(TARGET_INSTALL_DIR ${INTERFACE_INSTALL_DIR})
set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT})
manually_install_ssl_eay()
manually_install_openssl_for_qt()
package_libraries_for_deployment()
endif()

View file

@ -18,7 +18,7 @@ Original.Button {
id: button
property int color: 0
width: 120
height: 30
height: 28
style: ButtonStyle {

View file

@ -0,0 +1,70 @@
//
// CheckBox.qml
//
// Created by David Rowe on 26 Feb 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 as Original
import QtQuick.Controls.Styles 1.4
import "../styles-uit"
Original.CheckBox {
id: checkBox
HifiConstants { id: hifi }
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
readonly property int boxSize: 14
readonly property int boxRadius: 3
readonly property int checkSize: 10
readonly property int checkRadius: 2
style: CheckBoxStyle {
indicator: Rectangle {
id: box
width: boxSize
height: boxSize
radius: boxRadius
gradient: Gradient {
GradientStop {
position: 0.2
color: pressed || hovered
? (checkBox.isLightColorScheme ? hifi.colors.checkboxDarkStart : hifi.colors.checkboxLightStart)
: (checkBox.isLightColorScheme ? hifi.colors.checkboxLightStart : hifi.colors.checkboxDarkStart)
}
GradientStop {
position: 1.0
color: pressed || hovered
? (checkBox.isLightColorScheme ? hifi.colors.checkboxDarkFinish : hifi.colors.checkboxLightFinish)
: (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish)
}
}
Rectangle {
id: check
width: checkSize
height: checkSize
radius: checkRadius
anchors.centerIn: parent
color: hifi.colors.checkboxChecked
border.width: 1
border.color: hifi.colors.checkboxCheckedBorder
visible: checked && !pressed || !checked && pressed
}
}
label: Label {
text: control.text
colorScheme: checkBox.colorScheme
x: checkBox.boxSize / 2
wrapMode: Text.Wrap
}
}
}

View file

@ -0,0 +1,202 @@
//
// ComboBox.qml
//
// Created by Bradley Austin David on 27 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../styles-uit"
import "../controls-uit" as HifiControls
import "." as VrControls
FocusScope {
id: root
property alias model: comboBox.model;
property alias comboBox: comboBox
readonly property alias currentText: comboBox.currentText;
property alias currentIndex: comboBox.currentIndex;
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property string label: ""
property real controlHeight: height + (comboBoxLabel.visible ? comboBoxLabel.height + comboBoxLabel.anchors.bottomMargin : 0)
readonly property ComboBox control: comboBox
implicitHeight: comboBox.height;
focus: true
Rectangle {
id: background
gradient: Gradient {
GradientStop {
position: 0.2
color: popup.visible
? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark)
: (isLightColorScheme ? hifi.colors.dropDownLightStart : hifi.colors.dropDownDarkStart)
}
GradientStop {
position: 1.0
color: popup.visible
? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark)
: (isLightColorScheme ? hifi.colors.dropDownLightFinish : hifi.colors.dropDownDarkFinish)
}
}
anchors.fill: parent
}
SystemPalette { id: palette }
ComboBox {
id: comboBox
anchors.fill: parent
visible: false
height: hifi.fontSizes.textFieldInput + 14 // Match height of TextField control.
}
FiraSansSemiBold {
id: textField
anchors {
left: parent.left
leftMargin: hifi.dimensions.textPadding
right: dropIcon.left
verticalCenter: parent.verticalCenter
}
size: hifi.fontSizes.textFieldInput
text: comboBox.currentText
elide: Text.ElideRight
color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText )
}
Item {
id: dropIcon
anchors { right: parent.right; verticalCenter: parent.verticalCenter }
height: background.height
width: height
Rectangle {
width: 1
height: parent.height
anchors.top: parent.top
anchors.left: parent.left
color: isLightColorScheme ? hifi.colors.faintGray : hifi.colors.baseGray
}
HiFiGlyphs {
anchors {
top: parent.top
topMargin: -8
horizontalCenter: parent.horizontalCenter
}
size: hifi.dimensions.spinnerSize
text: hifi.glyphs.caratDn
color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText)
}
}
MouseArea {
id: controlHover
hoverEnabled: true
anchors.fill: parent
onClicked: toggleList();
}
function toggleList() {
if (popup.visible) {
hideList();
} else {
showList();
}
}
function showList() {
var r = desktop.mapFromItem(root, 0, 0, root.width, root.height);
listView.currentIndex = root.currentIndex
scrollView.x = r.x;
scrollView.y = r.y + r.height;
var bottom = scrollView.y + scrollView.height;
if (bottom > desktop.height) {
scrollView.y -= bottom - desktop.height + 8;
}
popup.visible = true;
popup.forceActiveFocus();
}
function hideList() {
popup.visible = false;
}
FocusScope {
id: popup
parent: desktop
anchors.fill: parent
z: desktop.zLevels.menu
visible: false
focus: true
MouseArea {
anchors.fill: parent
onClicked: hideList();
}
function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; }
function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; }
function selectCurrentItem() { root.currentIndex = listView.currentIndex; hideList(); }
function selectSpecificItem(index) { root.currentIndex = index; hideList(); }
Keys.onUpPressed: previousItem();
Keys.onDownPressed: nextItem();
Keys.onSpacePressed: selectCurrentItem();
Keys.onRightPressed: selectCurrentItem();
Keys.onReturnPressed: selectCurrentItem();
Keys.onEscapePressed: hideList();
ScrollView {
id: scrollView
height: 480
ListView {
id: listView
height: textField.height * count * 1.4
model: root.model
delegate: Rectangle {
width: root.width + 4
height: popupText.implicitHeight * 1.4
color: popupHover.containsMouse ? hifi.colors.primaryHighlight : (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark)
FiraSansSemiBold {
anchors.left: parent.left
anchors.leftMargin: hifi.dimensions.textPadding
anchors.verticalCenter: parent.verticalCenter
id: popupText
text: listView.model[index]
size: hifi.fontSizes.textFieldInput
color: hifi.colors.baseGray
}
MouseArea {
id: popupHover
anchors.fill: parent;
hoverEnabled: true
onEntered: listView.currentIndex = index;
onClicked: popup.selectSpecificItem(index)
}
}
}
}
}
HifiControls.Label {
id: comboBoxLabel
text: root.label
colorScheme: root.colorScheme
anchors.left: parent.left
anchors.bottom: parent.top
anchors.bottomMargin: 4
visible: label != ""
}
}

View file

@ -0,0 +1,133 @@
//
// ContentSection.qml
//
// Created by David Rowe on 16 Feb 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 QtGraphicalEffects 1.0
import "../styles-uit"
Column {
property string name: "Static Section"
property bool isFirst: false
property bool isCollapsible: false // Set at creation.
property bool isCollapsed: false
spacing: 0 // Defer spacing decisions to individual controls.
anchors {
left: parent.left
leftMargin: hifi.dimensions.contentMargin.x
right: parent.right
rightMargin: hifi.dimensions.contentMargin.x
}
function toggleCollapsed() {
if (isCollapsible) {
isCollapsed = !isCollapsed;
for (var i = 1; i < children.length; i++) {
children[i].visible = !isCollapsed;
}
}
}
Item {
id: sectionName
anchors.left: parent.left
anchors.right: parent.right
height: leadingSpace.height + topBar.height + heading.height + bottomBar.height
Item {
id: leadingSpace
width: 1
height: isFirst ? hifi.dimensions.contentSpacing.y : hifi.dimensions.controlInterlineHeight
anchors.top: parent.top
}
Item {
id: topBar
visible: !isFirst
height: visible ? 2 : 0
anchors.top: leadingSpace.bottom
Rectangle {
id: shadow
width: frame.width
height: 1
color: hifi.colors.baseGrayShadow
x: -hifi.dimensions.contentMargin.x
}
Rectangle {
width: frame.width
height: 1
color: hifi.colors.baseGrayHighlight
x: -hifi.dimensions.contentMargin.x
anchors.top: shadow.bottom
}
}
Item {
id: heading
anchors {
left: parent.left
right: parent.right
top: topBar.bottom
}
height: (isCollapsible ? 3 : 2) * hifi.dimensions.contentSpacing.y
RalewayRegular {
id: title
anchors {
left: parent.left
top: parent.top
topMargin: hifi.dimensions.contentSpacing.y
}
size: hifi.fontSizes.sectionName
font.capitalization: Font.AllUppercase
text: name
color: hifi.colors.lightGrayText
}
HiFiGlyphs {
anchors {
verticalCenter: title.verticalCenter
right: parent.right
rightMargin: -4
}
y: -2
size: hifi.fontSizes.disclosureButton
text: isCollapsed ? hifi.glyphs.disclosureButtonExpand : hifi.glyphs.disclosureButtonCollapse
color: hifi.colors.lightGrayText
visible: isCollapsible
}
MouseArea {
anchors.fill: parent
onClicked: toggleCollapsed()
}
}
LinearGradient {
id: bottomBar
visible: isCollapsible
width: frame.width
height: visible ? 4 : 0
x: -hifi.dimensions.contentMargin.x
anchors.top: heading.bottom
start: Qt.point(0, 0)
end: Qt.point(0, 4)
gradient: Gradient {
GradientStop { position: 0.0; color: hifi.colors.darkGray }
GradientStop { position: 1.0; color: hifi.colors.baseGray } // Equivalent of darkGray0 over baseGray background.
}
cached: true
}
}
}

View file

@ -0,0 +1,20 @@
//
// Label.qml
//
// Created by David Rowe on 26 Feb 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 "../styles-uit"
RalewaySemibold {
property int colorScheme: hifi.colorSchemes.light
size: hifi.fontSizes.inputLabel
color: colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGray : hifi.colors.lightGrayText
}

View file

@ -0,0 +1,98 @@
//
// Slider.qml
//
// Created by David Rowe on 27 Feb 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 QtQuick.Controls.Styles 1.4
import "../styles-uit"
import "../controls-uit" as HifiControls
Slider {
id: slider
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property string label: ""
property real controlHeight: height + (sliderLabel.visible ? sliderLabel.height + sliderLabel.anchors.bottomMargin : 0)
height: hifi.fontSizes.textFieldInput + 14 // Match height of TextField control.
y: sliderLabel.visible ? sliderLabel.height + sliderLabel.anchors.bottomMargin : 0
style: SliderStyle {
groove: Rectangle {
implicitWidth: 50
implicitHeight: hifi.dimensions.sliderGrooveHeight
radius: height / 2
color: isLightColorScheme ? hifi.colors.sliderGutterLight : hifi.colors.sliderGutterDark
Rectangle {
width: parent.height - 2
height: slider.value * slider.width - 1
radius: height / 2
anchors {
top: parent.top
topMargin: width + 1
left: parent.left
leftMargin: 1
}
transformOrigin: Item.TopLeft
rotation: -90
gradient: Gradient {
GradientStop { position: 0.0; color: hifi.colors.blueAccent }
GradientStop { position: 1.0; color: hifi.colors.primaryHighlight }
}
}
}
handle: Rectangle {
implicitWidth: hifi.dimensions.sliderHandleSize
implicitHeight: hifi.dimensions.sliderHandleSize
radius: height / 2
border.width: 1
border.color: isLightColorScheme ? hifi.colors.sliderBorderLight : hifi.colors.sliderBorderDark
gradient: Gradient {
GradientStop {
position: 0.0
color: pressed || hovered
? (isLightColorScheme ? hifi.colors.sliderDarkStart : hifi.colors.sliderLightStart )
: (isLightColorScheme ? hifi.colors.sliderLightStart : hifi.colors.sliderDarkStart )
}
GradientStop {
position: 1.0
color: pressed || hovered
? (isLightColorScheme ? hifi.colors.sliderDarkFinish : hifi.colors.sliderLightFinish )
: (isLightColorScheme ? hifi.colors.sliderLightFinish : hifi.colors.sliderDarkFinish )
}
}
Rectangle {
height: parent.height - 2
width: height
radius: height / 2
anchors.centerIn: parent
color: hifi.colors.transparent
border.width: 1
border.color: hifi.colors.black
}
}
}
HifiControls.Label {
id: sliderLabel
text: slider.label
colorScheme: slider.colorScheme
anchors.left: parent.left
anchors.bottom: parent.top
anchors.bottomMargin: 2
visible: label != ""
}
}

View file

@ -0,0 +1,79 @@
//
// SpinBox.qml
//
// Created by David Rowe on 26 Feb 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 QtQuick.Controls.Styles 1.4
import "../styles-uit"
import "../controls-uit" as HifiControls
SpinBox {
id: spinBox
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property string label: ""
property real controlHeight: height + (spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0)
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.fontSizes.textFieldInput + 14 // Match height of TextField control.
y: spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0
style: SpinBoxStyle {
background: Rectangle {
color: isLightColorScheme
? (spinBox.focus ? hifi.colors.white : hifi.colors.lightGray)
: (spinBox.focus ? hifi.colors.black : hifi.colors.baseGrayShadow)
border.color: hifi.colors.primaryHighlight
border.width: spinBox.focus ? 1 : 0
}
textColor: isLightColorScheme
? (spinBox.focus ? hifi.colors.black : hifi.colors.lightGray)
: (spinBox.focus ? hifi.colors.white : hifi.colors.lightGrayText)
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
horizontalAlignment: Qt.AlignLeft
padding.left: hifi.dimensions.textPadding
padding.right: hifi.dimensions.spinnerSize
incrementControl: HiFiGlyphs {
id: incrementButton
text: hifi.glyphs.caratUp
x: 6
y: 2
size: hifi.dimensions.spinnerSize
color: styleData.upPressed ? (isLightColorScheme ? hifi.colors.black : hifi.colors.white) : hifi.colors.gray
}
decrementControl: HiFiGlyphs {
text: hifi.glyphs.caratDn
x: 6
y: -3
size: hifi.dimensions.spinnerSize
color: styleData.downPressed ? (isLightColorScheme ? hifi.colors.black : hifi.colors.white) : hifi.colors.gray
}
}
HifiControls.Label {
id: spinBoxLabel
text: spinBox.label
colorScheme: spinBox.colorScheme
anchors.left: parent.left
anchors.bottom: parent.top
anchors.bottomMargin: 4
visible: label != ""
}
}

View file

@ -1,65 +0,0 @@
//
// StaticSection.qml
//
// Created by David Rowe on 16 Feb 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 "../styles-uit"
Column {
property string name: "Static Section"
property bool hasSeparator: false
spacing: hifi.dimensions.contentSpacing.y
anchors {
left: parent.left
leftMargin: hifi.dimensions.contentMargin.x
right: parent.right
rightMargin: hifi.dimensions.contentMargin.x
}
VerticalSpacer { }
Item {
visible: hasSeparator
anchors.top: sectionName.top
Rectangle {
width: frame.width
height: 1
color: hifi.colors.baseGrayShadow
x: -hifi.dimensions.contentMargin.x
anchors.bottom: highlight.top
}
Rectangle {
id: highlight
width: frame.width
height: 1
color: hifi.colors.baseGrayHighlight
x: -hifi.dimensions.contentMargin.x
anchors.bottom: parent.top
}
}
RalewayRegular {
id: sectionName
text: parent.name
size: hifi.fontSizes.sectionName
font.capitalization: Font.AllUppercase
color: hifi.colors.lightGrayText
verticalAlignment: Text.AlignBottom
height: {
if (hasSeparator) {
hifi.dimensions.contentMargin.y
}
}
}
}

View file

@ -133,13 +133,14 @@ TableView {
HiFiGlyphs {
id: reloadButton
text: hifi.glyphs.reloadSmall
color: parent.color
color: reloadButtonArea.pressed ? hifi.colors.white : parent.color
anchors {
top: parent.top
right: stopButton.left
verticalCenter: parent.verticalCenter
}
MouseArea {
id: reloadButtonArea
anchors { fill: parent; margins: -2 }
onClicked: reloadScript(model.url)
}
@ -149,13 +150,14 @@ TableView {
HiFiGlyphs {
id: stopButton
text: hifi.glyphs.closeSmall
color: parent.color
color: stopButtonArea.pressed ? hifi.colors.white : parent.color
anchors {
top: parent.top
right: parent.right
verticalCenter: parent.verticalCenter
}
MouseArea {
id: stopButtonArea
anchors { fill: parent; margins: -2 }
onClicked: stopScript(model.url)
}

View file

@ -13,25 +13,31 @@ import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../styles-uit"
import "../controls-uit" as HifiControls
TextField {
id: textField
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property string label: ""
property real controlHeight: height + (textFieldLabel.visible ? textFieldLabel.height : 0)
placeholderText: textField.placeholderText
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: implicitHeight + 4 // Make surrounding box higher so that highlight is vertically centered.
placeholderText: textField.label // Instead of separate label (see below).
y: textFieldLabel.visible ? textFieldLabel.height + textFieldLabel.anchors.bottomMargin : 0
style: TextFieldStyle {
textColor: textField.colorScheme == hifi.colorSchemes.light
textColor: isLightColorScheme
? (textField.focus ? hifi.colors.black : hifi.colors.lightGray)
: (textField.focus ? hifi.colors.white : hifi.colors.lightGrayText)
background: Rectangle {
color: textField.colorScheme == hifi.colorSchemes.light
color: isLightColorScheme
? (textField.focus ? hifi.colors.white : hifi.colors.lightGray)
: (textField.focus ? hifi.colors.black : hifi.colors.baseGrayShadow)
border.color: hifi.colors.primaryHighlight
@ -44,16 +50,13 @@ TextField {
padding.right: hifi.dimensions.textPadding
}
/*
// Separate label instead of placeholderText.
RalewaySemibold {
HifiControls.Label {
id: textFieldLabel
text: textField.label
size: hifi.fontSizes.inputLabel
color: hifi.colors.lightGrayText
colorScheme: textField.colorScheme
anchors.left: parent.left
anchors.bottom: parent.top
anchors.bottomMargin: 4
visible: label != ""
}
*/
}

View file

@ -55,11 +55,11 @@ TreeView {
alternateBackgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd
branchDelegate: HiFiGlyphs {
text: styleData.isExpanded ? hifi.glyphs.disclosureCollapse : hifi.glyphs.disclosureExpand
size: hifi.fontSizes.tableText * 2.5 // tableText is in points; proportionately scale to pixels
text: styleData.isExpanded ? hifi.glyphs.caratDn : hifi.glyphs.caratR
size: hifi.fontSizes.carat
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
anchors {
left: parent ? parent.left : undefined
leftMargin: hifi.dimensions.tablePadding / 2

View file

@ -14,5 +14,5 @@ import "../styles-uit"
Item {
width: 1 // Must be non-zero
height: hifi.dimensions.contentSpacing.y
height: hifi.dimensions.controlInterlineHeight
}

View file

@ -1,10 +1,19 @@
//
// PreferencesDialog.qml
//
// Created by Bradley Austin Davis on 24 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2 as OriginalDialogs
import Qt.labs.settings 1.0
import "../controls" as HifiControls
import "../windows"
import "../controls-uit" as HifiControls
import "../styles-uit"
import "../windows-uit"
import "preferences"
Window {
@ -16,6 +25,9 @@ Window {
height: 577
property var sections: []
property var showCategories: []
minSize: Qt.vector2d(400, 500)
HifiConstants { id: hifi }
function saveAll() {
for (var i = 0; i < sections.length; ++i) {
@ -33,10 +45,8 @@ Window {
destroy();
}
Rectangle {
anchors.fill: parent
clip: true
color: "white"
Column {
width: pane.contentWidth
Component {
id: sectionBuilder
@ -64,45 +74,45 @@ Window {
}
if (sections.length) {
sections[0].expanded = true;
// Default sections to expanded/collapsed as appropriate for dialog.
if (sections.length === 1) {
sections[0].collapsable = false
sections[0].expanded = true
} else {
for (i = 0; i < sections.length; i++) {
sections[i].collapsable = true;
sections[i].expanded = true;
}
}
sections[0].isFirst = true;
sections[sections.length - 1].isLast = true;
}
}
Flickable {
id: flickable
clip: true
interactive: true
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: dialogButtons.top
anchors.bottomMargin: 8
contentHeight: prefControls.height
contentWidth: parent.width
Column {
id: prefControls
anchors.left: parent.left
anchors.right: parent.right
}
Column {
id: prefControls
width: pane.contentWidth
}
Row {
id: dialogButtons
anchors { bottom: parent.bottom; right: parent.right; margins: 8 }
}
Button {
text: "Cancel";
onClicked: root.restoreAll();
}
footer: Row {
anchors {
right: parent.right;
rightMargin: hifi.dimensions.contentMargin.x
verticalCenter: parent.verticalCenter
}
spacing: hifi.dimensions.contentSpacing.x
Button {
text: "Save all changes"
onClicked: root.saveAll();
}
HifiControls.Button {
text: "Save changes"
color: hifi.buttons.blue
onClicked: root.saveAll()
}
HifiControls.Button {
text: "Cancel"
color: hifi.buttons.white
onClicked: root.restoreAll()
}
}
}

View file

@ -1,15 +1,25 @@
//
// AvatarPreference.qml
//
// Created by Bradley Austin Davis on 22 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../../dialogs"
import "../../controls-uit"
Preference {
id: root
property alias buttonText: button.text
property alias text: dataTextField.text
property alias buttonText: button.text
property alias placeholderText: dataTextField.placeholderText
property real spacing: 8
property var browser;
height: labelText.height + Math.max(dataTextField.height, button.height) + spacing
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
dataTextField.text = preference.value;
@ -41,40 +51,48 @@ Preference {
preference.save();
}
Text {
id: labelText
color: enabled ? "black" : "gray"
text: root.label
}
TextField {
id: dataTextField
placeholderText: root.placeholderText
text: preference.value
style: TextFieldStyle { renderType: Text.QtRendering }
Item {
id: control
anchors {
top: labelText.bottom
left: parent.left
right: button.left
topMargin: root.spacing
rightMargin: root.spacing
right: parent.right
bottom: parent.bottom
}
}
height: Math.max(dataTextField.controlHeight, button.height)
Component {
id: avatarBrowserBuilder;
AvatarBrowser { }
}
Button {
id: button
anchors { right: parent.right; verticalCenter: dataTextField.verticalCenter }
text: "Browse"
onClicked: {
root.browser = avatarBrowserBuilder.createObject(desktop);
root.browser.windowDestroyed.connect(function(){
root.browser = null;
})
TextField {
id: dataTextField
placeholderText: root.placeholderText
text: preference.value
label: root.label
anchors {
left: parent.left
right: button.left
rightMargin: hifi.dimensions.contentSpacing.x
bottom: parent.bottom
}
colorScheme: hifi.colorSchemes.dark
}
Component {
id: avatarBrowserBuilder;
AvatarBrowser { }
}
Button {
id: button
text: "Browse"
anchors {
right: parent.right
verticalCenter: dataTextField.verticalCenter
}
onClicked: {
root.browser = avatarBrowserBuilder.createObject(desktop);
root.browser.windowDestroyed.connect(function(){
root.browser = null;
})
}
}
}
}

View file

@ -1,16 +1,23 @@
//
// BrowsablePreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../../dialogs"
import "../../controls-uit"
Preference {
id: root
property alias buttonText: button.text
property alias text: dataTextField.text
property alias placeholderText: dataTextField.placeholderText
property real spacing: 8
height: labelText.height + Math.max(dataTextField.height, button.height) + spacing
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
dataTextField.text = preference.value;
@ -21,42 +28,49 @@ Preference {
preference.save();
}
Text {
id: labelText
color: enabled ? "black" : "gray"
text: root.label
}
TextField {
id: dataTextField
placeholderText: root.placeholderText
text: preference.value
style: TextFieldStyle { renderType: Text.QtRendering }
Item {
id: control
anchors {
top: labelText.bottom
left: parent.left
right: button.left
topMargin: root.spacing
rightMargin: root.spacing
right: parent.right
bottom: parent.bottom
}
}
height: Math.max(dataTextField.controlHeight, button.height)
Component {
id: fileBrowserBuilder;
FileDialog { selectDirectory: true }
}
TextField {
id: dataTextField
Button {
id: button
anchors { right: parent.right; verticalCenter: dataTextField.verticalCenter }
text: preference.browseLabel
onClicked: {
var browser = fileBrowserBuilder.createObject(desktop, { selectDirectory: true, folder: fileDialogHelper.pathToUrl(preference.value) });
browser.selectedFile.connect(function(fileUrl){
console.log(fileUrl);
dataTextField.text = fileDialogHelper.urlToPath(fileUrl);
});
anchors {
left: parent.left
right: button.left
rightMargin: hifi.dimensions.contentSpacing.x
bottom: parent.bottom
}
label: root.label
placeholderText: root.placeholderText
colorScheme: hifi.colorSchemes.dark
}
Component {
id: fileBrowserBuilder;
FileDialog { selectDirectory: true }
}
Button {
id: button
text: preference.browseLabel
anchors {
right: parent.right
verticalCenter: dataTextField.verticalCenter
}
onClicked: {
var browser = fileBrowserBuilder.createObject(desktop, { selectDirectory: true, folder: fileDialogHelper.pathToUrl(preference.value) });
browser.selectedFile.connect(function(fileUrl){
console.log(fileUrl);
dataTextField.text = fileDialogHelper.urlToPath(fileUrl);
});
}
}
}
}

View file

@ -1,16 +1,29 @@
//
// ButtonPreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4 as Original
import "../../controls-uit"
Preference {
id: root
height: button.height
height: button.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: button.text = preference.name;
function save() { }
Original.Button {
Button {
id: button
onClicked: preference.trigger()
width: 180
anchors.bottom: parent.bottom
}
}

View file

@ -1,10 +1,20 @@
//
// CheckBoxPreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../../controls"
import "../../controls-uit"
Preference {
id: root
height: checkBox.implicitHeight
height: spacer.height + Math.max(hifi.dimensions.controlLineHeight, checkBox.implicitHeight)
Component.onCompleted: {
checkBox.checked = preference.value;
@ -16,9 +26,25 @@ Preference {
preference.save();
}
Item {
id: spacer
anchors {
top: parent.top
left: parent.left
right: parent.right
}
height: isFirstCheckBox ? hifi.dimensions.controlInterlineHeight : 0
}
CheckBox {
id: checkBox
anchors.fill: parent
anchors {
top: spacer.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
}
text: root.label
colorScheme: hifi.colorSchemes.dark
}
}

View file

@ -1,14 +1,26 @@
//
// ComboBoxPreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../../controls-uit" as HiFiControls
import "../../styles-uit"
Preference {
id: root
property real spacing: 8
height: labelText.height + dataComboBox.height + spacing
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
dataComboBox.currentIndex = dataComboBox.find(preference.value);
dataComboBox.currentIndex = dataComboBox.comboBox.find(preference.value);
}
function save() {
@ -16,22 +28,38 @@ Preference {
preference.save();
}
Text {
id: labelText
color: enabled ? "black" : "gray"
text: root.label
}
ComboBox {
id: dataComboBox
model: preference.items
style: ComboBoxStyle { renderType: Text.QtRendering }
Item {
id: control
anchors {
top: labelText.bottom
left: parent.left
right: parent.right
topMargin: root.spacing
rightMargin: root.spacing
bottom: parent.bottom
}
height: Math.max(labelText.height, dataComboBox.controlHeight)
HiFiControls.Label {
id: labelText
text: root.label + ":"
colorScheme: hifi.colorSchemes.dark
anchors {
left: parent.left
right: dataComboBox.left
rightMargin: hifi.dimensions.labelPadding
verticalCenter: parent.verticalCenter
}
horizontalAlignment: Text.AlignRight
wrapMode: Text.Wrap
}
HiFiControls.ComboBox {
id: dataComboBox
model: preference.items
width: 150
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
colorScheme: hifi.colorSchemes.dark
}
}
}

View file

@ -1,11 +1,21 @@
//
// EditablePreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../../dialogs"
import "../../controls-uit"
Preference {
id: root
property real spacing: 8
height: labelText.height + dataTextField.height + spacing
height: dataTextField.controlHeight + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
dataTextField.text = preference.value;
@ -16,22 +26,16 @@ Preference {
preference.save();
}
Text {
id: labelText
color: enabled ? "black" : "gray"
text: root.label
}
TextField {
id: dataTextField
placeholderText: preference.placeholderText
style: TextFieldStyle { renderType: Text.QtRendering }
label: root.label
colorScheme: hifi.colorSchemes.dark
anchors {
top: labelText.bottom
left: parent.left
right: parent.right
topMargin: root.spacing
rightMargin: root.spacing
bottom: parent.bottom
}
}
}

View file

@ -1,3 +1,13 @@
//
// Preference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
@ -6,6 +16,7 @@ Item {
anchors { left: parent.left; right: parent.right }
property var preference;
property string label: preference ? preference.name : "";
property bool isFirstCheckBox;
Component.onCompleted: {
if (preference) {
preference.load();

View file

@ -1,20 +1,33 @@
//
// Section.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import Hifi 1.0
import "../../controls" as VrControls
import "../../controls-uit" as HiFiControls
import "../../styles-uit"
import "."
Preference {
id: root
property bool collapsable: true
property bool expanded: false
property bool isFirst: false
property bool isLast: false
property string name: "Header"
property real spacing: 8
readonly property alias toggle: toggle
readonly property alias header: header
default property alias preferences: contentContainer.children
HifiConstants { id: hifi }
function saveAll() {
for (var i = 0; i < d.preferences.length; ++i) {
var preference = d.preferences[i];
@ -29,47 +42,24 @@ Preference {
}
}
clip: true
children: [ toggle, header, contentContainer ]
height: expanded ? header.height + contentContainer.height + root.spacing * 3
: Math.max(toggle.height, header.height) + root.spacing * 2
Behavior on height { PropertyAnimation {} }
children: [ contentContainer ]
height: contentContainer.height + (root.isLast ? 2 * hifi.dimensions.contentSpacing.y : 0)
Component.onCompleted: d.buildPreferences();
function toggleExpanded() {
root.expanded = !root.expanded;
}
VrControls.FontAwesome {
id: toggle
width: root.collapsable ? height : 0
anchors { left: parent.left; top: parent.top; margins: root.spacing }
visible: root.collapsable
enabled: root.collapsable
rotation: root.expanded ? 0 : -90
text: "\uf078"
Behavior on rotation { PropertyAnimation {} }
MouseArea { anchors.fill: parent; onClicked: root.toggleExpanded() }
}
Text {
id: header
anchors { left: toggle.right; top: parent.top; leftMargin: root.spacing * 2; margins: root.spacing }
font.bold: true
font.pointSize: 16
color: "#0e7077"
text: root.name
MouseArea { anchors.fill: parent; onClicked: root.toggleExpanded() }
}
Column {
HiFiControls.ContentSection {
id: contentContainer
spacing: root.spacing
anchors { left: toggle.right; top: header.bottom; topMargin: root.spacing; right: parent.right; margins: root.spacing }
enabled: root.expanded
visible: root.expanded
clip: true
name: root.name
isFirst: root.isFirst
isCollapsible: root.collapsable
isCollapsed: !root.expanded
anchors {
left: parent.left
right: parent.right
margins: 0
}
}
QtObject {
@ -83,6 +73,7 @@ Preference {
property var buttonBuilder: Component { ButtonPreference { } }
property var comboBoxBuilder: Component { ComboBoxPreference { } }
property var preferences: []
property int checkBoxCount: 0
function buildPreferences() {
var categoryPreferences = Preferences.preferencesByCategory[root.name];
@ -99,40 +90,49 @@ Preference {
var builder;
switch (preference.type) {
case Preference.Editable:
checkBoxCount = 0;
builder = editableBuilder;
break;
case Preference.Browsable:
checkBoxCount = 0;
builder = browsableBuilder;
break;
case Preference.Spinner:
checkBoxCount = 0;
builder = spinnerBuilder;
break;
case Preference.Slider:
checkBoxCount = 0;
builder = sliderBuilder;
break;
case Preference.Checkbox:
checkBoxCount++;
console.log("####### checkBoxCount = " + checkBoxCount);
builder = checkboxBuilder;
break;
case Preference.Avatar:
checkBoxCount = 0;
builder = avatarBuilder;
break;
case Preference.Button:
checkBoxCount = 0;
builder = buttonBuilder;
break;
case Preference.ComboBox:
checkBoxCount = 0;
builder = comboBoxBuilder;
break;
};
if (builder) {
preferences.push(builder.createObject(contentContainer, { preference: preference }));
preferences.push(builder.createObject(contentContainer, { preference: preference, isFirstCheckBox: (checkBoxCount === 1) }));
}
}
}

View file

@ -1,10 +1,22 @@
//
// SpinBoxPreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../../dialogs"
import "../../controls-uit"
Preference {
id: root
property alias slider: slider
height: slider.height
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
slider.value = preference.value;
@ -15,16 +27,38 @@ Preference {
preference.save();
}
Text {
text: root.label
color: enabled ? "black" : "gray"
anchors.verticalCenter: slider.verticalCenter
}
Item {
id: control
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Math.max(labelText.height, slider.height)
Slider {
id: slider
value: preference.value
width: 130
anchors { right: parent.right }
Label {
id: labelText
text: root.label + ":"
colorScheme: hifi.colorSchemes.dark
anchors {
left: parent.left
right: slider.left
rightMargin: hifi.dimensions.labelPadding
verticalCenter: parent.verticalCenter
}
horizontalAlignment: Text.AlignRight
wrapMode: Text.Wrap
}
Slider {
id: slider
value: preference.value
width: 130
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
colorScheme: hifi.colorSchemes.dark
}
}
}

View file

@ -1,10 +1,21 @@
//
// SpinBoxPreference.qml
//
// Created by Bradley Austin Davis on 18 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../../controls-uit"
Preference {
id: root
property alias spinner: spinner
height: spinner.height
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
spinner.value = preference.value;
@ -15,18 +26,40 @@ Preference {
preference.save();
}
Text {
text: root.label
color: root.enabled ? "black" : "gray"
anchors.verticalCenter: spinner.verticalCenter
}
Item {
id: control
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Math.max(spinnerLabel.height, spinner.controlHeight)
SpinBox {
id: spinner
decimals: preference.decimals
minimumValue: preference.min
maximumValue: preference.max
width: 100
anchors { right: parent.right }
Label {
id: spinnerLabel
text: root.label + ":"
colorScheme: hifi.colorSchemes.dark
anchors {
left: parent.left
right: spinner.left
rightMargin: hifi.dimensions.labelPadding
verticalCenter: parent.verticalCenter
}
horizontalAlignment: Text.AlignRight
wrapMode: Text.Wrap
}
SpinBox {
id: spinner
decimals: preference.decimals
minimumValue: preference.min
maximumValue: preference.max
width: 100
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
colorScheme: hifi.colorSchemes.dark
}
}
}

View file

@ -6,7 +6,7 @@ import "../../dialogs"
PreferencesDialog {
id: root
objectName: "AudioPreferencesDialog"
title: "Audio Preferences"
title: "Audio Settings"
showCategories: ["Audio"]
property var settings: Settings {
category: root.objectName

View file

@ -6,7 +6,7 @@ import "../../dialogs"
PreferencesDialog {
id: root
objectName: "AvatarPreferencesDialog"
title: "Avatar Preferences"
title: "Avatar Settings"
showCategories: [ "Avatar Basics", "Avatar Tuning", "Avatar Camera" ]
property var settings: Settings {
category: root.objectName

View file

@ -1,3 +1,13 @@
//
// PreferencesDialog.qml
//
// Created by Bradley Austin Davis on 24 Jan 2016
// Copyright 2015 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 Qt.labs.settings 1.0
@ -6,7 +16,7 @@ import "../../dialogs"
PreferencesDialog {
id: root
objectName: "GeneralPreferencesDialog"
title: "General Preferences"
title: "General Settings"
showCategories: ["Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers"]
property var settings: Settings {
category: root.objectName

View file

@ -6,7 +6,7 @@ import "../../dialogs"
PreferencesDialog {
id: root
objectName: "GraphicsPreferencesDialog"
title: "Graphics Preferences"
title: "Graphics Settings"
showCategories: ["Graphics"]
property var settings: Settings {
category: root.objectName

View file

@ -6,7 +6,7 @@ import "../../dialogs"
PreferencesDialog {
id: root
objectName: "LodPreferencesDialog"
title: "Level of Detail preferences"
title: "LOD Settings"
showCategories: ["Level of Detail Tuning"]
property var settings: Settings {
category: root.objectName

View file

@ -24,7 +24,7 @@ Window {
resizable: true
destroyOnInvisible: true
x: 40; y: 40
implicitWidth: 384; implicitHeight: 640
implicitWidth: 400; implicitHeight: 695
minSize: Qt.vector2d(200, 300)
HifiConstants { id: hifi }
@ -87,8 +87,11 @@ Window {
Column {
width: pane.contentWidth
HifiControls.StaticSection {
HifiControls.ContentSection {
name: "Currently Running"
isFirst: true
HifiControls.VerticalSpacer {}
Row {
spacing: hifi.dimensions.contentSpacing.x
@ -106,6 +109,8 @@ Window {
}
}
HifiControls.VerticalSpacer {}
HifiControls.Table {
tableModel: runningScriptsModel
height: 185
@ -113,11 +118,16 @@ Window {
anchors.left: parent.left
anchors.right: parent.right
}
HifiControls.VerticalSpacer {
height: 2 // Table view draws a little taller than it's height.
}
}
HifiControls.StaticSection {
HifiControls.ContentSection {
name: "Load Scripts"
hasSeparator: true
HifiControls.VerticalSpacer {}
Row {
spacing: hifi.dimensions.contentSpacing.x
@ -161,18 +171,21 @@ Window {
}
}
HifiControls.VerticalSpacer {}
HifiControls.TextField {
id: filterEdit
anchors.left: parent.left
anchors.right: parent.right
focus: true
colorScheme: hifi.colorSchemes.dark
//placeholderText: "filter"
label: "Filter"
placeholderText: "filter"
onTextChanged: scriptsModel.filterRegExp = new RegExp("^.*" + text + ".*$", "i")
Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i")
}
HifiControls.VerticalSpacer {}
HifiControls.Tree {
id: treeView
height: 155
@ -182,6 +195,8 @@ Window {
anchors.right: parent.right
}
HifiControls.VerticalSpacer {}
HifiControls.TextField {
id: selectedScript
anchors.left: parent.left

View file

@ -48,11 +48,13 @@ Item {
// Other colors
readonly property color white: "#ffffff"
readonly property color gray: "#808080"
readonly property color black: "#000000"
// Semitransparent
readonly property color white50: "#80ffffff"
readonly property color white30: "#4dffffff"
readonly property color white25: "#40ffffff"
readonly property color transparent: "#00ffffff"
// Control specific colors
readonly property color tableRowLightOdd: white50
@ -61,6 +63,26 @@ Item {
readonly property color tableRowDarkEven: "#a6181818"
readonly property color tableScrollHandle: "#707070"
readonly property color tableScrollBackground: "#323232"
readonly property color checkboxLightStart: "#ffffff"
readonly property color checkboxLightFinish: "#afafaf"
readonly property color checkboxDarkStart: "#7d7d7d"
readonly property color checkboxDarkFinish: "#6b6a6b"
readonly property color checkboxChecked: primaryHighlight
readonly property color checkboxCheckedBorder: "#36cdff"
readonly property color sliderGutterLight: "#d4d4d4"
readonly property color sliderGutterDark: "#252525"
readonly property color sliderBorderLight: "#afafaf"
readonly property color sliderBorderDark: "#7d7d7d"
readonly property color sliderLightStart: "#ffffff"
readonly property color sliderLightFinish: "#afafaf"
readonly property color sliderDarkStart: "#7d7d7d"
readonly property color sliderDarkFinish: "#6b6a6b"
readonly property color dropDownPressedLight: "#d4d4d4"
readonly property color dropDownPressedDark: "#afafaf"
readonly property color dropDownLightStart: "#ffffff"
readonly property color dropDownLightFinish: "#afafaf"
readonly property color dropDownDarkStart: "#7d7d7d"
readonly property color dropDownDarkFinish: "#6b6a6b"
}
Item {
@ -76,39 +98,54 @@ Item {
readonly property real borderWidth: largeScreen ? 2 : 1
readonly property vector2d contentMargin: Qt.vector2d(12, 24)
readonly property vector2d contentSpacing: Qt.vector2d(8, 12)
readonly property real labelPadding: 40
readonly property real textPadding: 8
readonly property real sliderHandleSize: 18
readonly property real sliderGrooveHeight: 8
readonly property real spinnerSize: 42
readonly property real tablePadding: 12
readonly property real tableRowHeight: largeScreen ? 26 : 23
readonly property vector2d modalDialogMargin: Qt.vector2d(50, 30)
readonly property real modalDialogTitleHeight: 40
readonly property real controlLineHeight: 29 // Height of spinbox control on 1920 x 1080 monitor
readonly property real controlInterlineHeight: 22 // 75% of controlLineHeight
}
Item {
id: fontSizes // In pixels
readonly property real overlayTitle: dimensions.largeScreen? 18 : 14
readonly property real tabName: dimensions.largeScreen? 12 : 10
readonly property real sectionName: dimensions.largeScreen? 12 : 10
readonly property real inputLabel: dimensions.largeScreen? 14 : 10
readonly property real textFieldInput: dimensions.largeScreen? 15 : 12
readonly property real tableText: dimensions.largeScreen? 15 : 12
readonly property real buttonLabel: dimensions.largeScreen? 13 : 9
readonly property real iconButton: dimensions.largeScreen? 13 : 9
readonly property real listItem: dimensions.largeScreen? 15 : 11
readonly property real tabularData: dimensions.largeScreen? 15 : 11
readonly property real logs: dimensions.largeScreen? 16 : 12
readonly property real code: dimensions.largeScreen? 16 : 12
readonly property real rootMenu: dimensions.largeScreen? 15 : 11
readonly property real menuItem: dimensions.largeScreen? 15 : 11
readonly property real shortcutText: dimensions.largeScreen? 13 : 9
readonly property real overlayTitle: dimensions.largeScreen ? 18 : 14
readonly property real tabName: dimensions.largeScreen ? 12 : 10
readonly property real sectionName: dimensions.largeScreen ? 12 : 10
readonly property real inputLabel: dimensions.largeScreen ? 14 : 10
readonly property real textFieldInput: dimensions.largeScreen ? 15 : 12
readonly property real tableText: dimensions.largeScreen ? 15 : 12
readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9
readonly property real iconButton: dimensions.largeScreen ? 13 : 9
readonly property real listItem: dimensions.largeScreen ? 15 : 11
readonly property real tabularData: dimensions.largeScreen ? 15 : 11
readonly property real logs: dimensions.largeScreen ? 16 : 12
readonly property real code: dimensions.largeScreen ? 16 : 12
readonly property real rootMenu: dimensions.largeScreen ? 15 : 11
readonly property real menuItem: dimensions.largeScreen ? 15 : 11
readonly property real shortcutText: dimensions.largeScreen ? 13 : 9
readonly property real carat: dimensions.largeScreen ? 38 : 30
readonly property real disclosureButton: dimensions.largeScreen ? 20 : 15
}
Item {
id: glyphs
readonly property string backward: "E"
readonly property string caratDn: "5"
readonly property string caratR: "3"
readonly property string caratUp: "6"
readonly property string close: "w"
readonly property string closeInverted: "x"
readonly property string closeSmall: "C"
readonly property string disclosureButtonCollapse: "M"
readonly property string disclosureButtonExpand: "L"
readonly property string disclosureCollapse: "Z"
readonly property string disclosureExpand: "B"
readonly property string forward: "D"
readonly property string pin: "y"
readonly property string pinInverted: "z"
readonly property string reloadSmall: "a"

View file

@ -99,8 +99,8 @@ Frame {
DropShadow {
source: titleText
anchors.fill: titleText
horizontalOffset: 1
verticalOffset: 1
horizontalOffset: 2
verticalOffset: 2
samples: 2
color: hifi.colors.baseGrayShadow60
visible: (window && window.focus)

View file

@ -58,6 +58,8 @@ Fadable {
// The content to place inside the window, determined by the client
default property var content
property var footer: Item { } // Optional static footer at the bottom of the dialog.
function setDefaultFocus() {} // Default function; can be overridden by dialogs.
property var rectifier: Timer {
@ -125,7 +127,8 @@ Fadable {
// Scrollable window content.
property var pane: Item {
property bool isScrolling: scrollView.height < scrollView.contentItem.height
property int contentWidth: scrollView.width - (isScrolling ? 11 : 0)
property int contentWidth: scrollView.width - (isScrolling ? 10 : 0)
property int scrollHeight: scrollView.height
anchors.fill: parent
anchors.rightMargin: isScrolling ? 11 : 0
@ -162,6 +165,7 @@ Fadable {
verticalScrollBarPolicy: Qt.ScrollBarAsNeeded
anchors.fill: parent
anchors.rightMargin: parent.isScrolling ? 1 : 0
anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0
style: ScrollViewStyle {
@ -203,7 +207,46 @@ Fadable {
}
}
}
Rectangle {
// Optional non-scrolling footer.
id: footerPane
anchors {
left: parent.left
bottom: parent.bottom
}
width: parent.contentWidth
height: footer.height + 2 * hifi.dimensions.contentSpacing.y
color: hifi.colors.baseGray
visible: footer.height > 0
Item {
// Horizontal rule.
anchors.fill: parent
Rectangle {
width: parent.width
height: 1
y: 1 // Stop displaying content just above horizontal rule/=.
color: hifi.colors.baseGrayShadow
}
Rectangle {
width: parent.width
height: 1
y: 2
color: hifi.colors.baseGrayHighlight
}
}
Item {
anchors.fill: parent
anchors.topMargin: 3 // Horizontal rule.
children: [ footer ]
}
}
}
children: [ swallower, frame, pane, activator ]
Component.onCompleted: { raise(); setDefaultFocus(); }

View file

@ -370,7 +370,6 @@ bool setupEssentials(int& argc, char** argv) {
DependencyManager::set<AutoUpdater>();
DependencyManager::set<PathUtils>();
DependencyManager::set<InterfaceActionFactory>();
DependencyManager::set<AssetClient>();
DependencyManager::set<AudioInjectorManager>();
DependencyManager::set<MessagesClient>();
DependencyManager::set<UserInputMapper>();
@ -528,13 +527,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
audioThread->start();
// Setup AssetClient
auto assetClient = DependencyManager::get<AssetClient>();
QThread* assetThread = new QThread;
assetThread->setObjectName("Asset Thread");
assetClient->moveToThread(assetThread);
connect(assetThread, &QThread::started, assetClient.data(), &AssetClient::init);
assetThread->start();
ResourceManager::init();
// Setup MessagesClient
auto messagesClient = DependencyManager::get<MessagesClient>();
@ -547,11 +540,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(connectedToDomain(const QString&)));
connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain()));
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
connect(&domainHandler, &DomainHandler::settingsReceived, this, &Application::domainSettingsReceived);
// update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one
const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * 1000;
@ -588,6 +580,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&accountManager, &AccountManager::usernameChanged, this, &Application::updateWindowTitle);
// set the account manager's root URL and trigger a login request if we don't have the access token
accountManager.setIsAgent(true);
accountManager.setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL);
UserActivityLogger::getInstance().launch(applicationVersion());
@ -644,13 +637,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&identityPacketTimer, &QTimer::timeout, getMyAvatar(), &MyAvatar::sendIdentityPacket);
identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkDiskCache* cache = new QNetworkDiskCache();
cache->setMaximumCacheSize(MAXIMUM_CACHE_SIZE);
cache->setCacheDirectory(!cachePath.isEmpty() ? cachePath : "interfaceCache");
networkAccessManager.setCache(cache);
ResourceCache::setRequestLimit(3);
_glWidget = new GLCanvas();
@ -661,15 +647,15 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
_glWidget->setFocusPolicy(Qt::StrongFocus);
_glWidget->setFocus();
#ifdef Q_OS_MAC
// OSX doesn't seem to provide for hiding the cursor only on the GL widget
_window->setCursor(Qt::BlankCursor);
auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget
#else
// On windows and linux, hiding the top level cursor also means it's invisible
// when hovering over the window menu, which is a pain, so only hide it for
// the GL surface
_glWidget->setCursor(Qt::BlankCursor);
// On windows and linux, hiding the top level cursor also means it's invisible when hovering over the
// window menu, which is a pain, so only hide it for the GL surface
auto cursorTarget = _glWidget;
#endif
cursorTarget->setCursor(Qt::BlankCursor);
// enable mouse tracking; otherwise, we only get drag events
_glWidget->setMouseTracking(true);
@ -903,9 +889,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
SpacemouseManager::getInstance().init();
#endif
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::DomainConnectionDenied, this, "handleDomainConnectionDeniedPacket");
// If the user clicks an an entity, we will check that it's an unlocked web entity, and if so, set the focus to it
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
@ -981,6 +964,29 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
_idleTimer->start(0);
}
void Application::checkChangeCursor() {
QMutexLocker locker(&_changeCursorLock);
if (_cursorNeedsChanging) {
#ifdef Q_OS_MAC
auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget
#else
// On windows and linux, hiding the top level cursor also means it's invisible when hovering over the
// window menu, which is a pain, so only hide it for the GL surface
auto cursorTarget = _glWidget;
#endif
cursorTarget->setCursor(_desiredCursor);
_cursorNeedsChanging = false;
}
}
void Application::showCursor(const QCursor& cursor) {
QMutexLocker locker(&_changeCursorLock);
_desiredCursor = cursor;
_cursorNeedsChanging = true;
}
void Application::aboutToQuit() {
emit beforeAboutToQuit();
@ -1062,13 +1068,6 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy<OffscreenUi>();
}
void Application::emptyLocalCache() {
if (auto cache = NetworkAccessManager::getInstance().cache()) {
qDebug() << "DiskCacheEditor::clear(): Clearing disk cache.";
cache->clear();
}
}
Application::~Application() {
EntityTreePointer tree = getEntities()->getTree();
tree->setSimulation(NULL);
@ -1106,11 +1105,7 @@ Application::~Application() {
DependencyManager::destroy<ScriptCache>();
DependencyManager::destroy<SoundCache>();
// cleanup the AssetClient thread
QThread* assetThread = DependencyManager::get<AssetClient>()->thread();
DependencyManager::destroy<AssetClient>();
assetThread->quit();
assetThread->wait();
ResourceManager::cleanup();
QThread* nodeThread = DependencyManager::get<NodeList>()->thread();
@ -2431,6 +2426,9 @@ void Application::idle(uint64_t now) {
return; // bail early, nothing to do here.
}
checkChangeCursor();
Stats::getInstance()->updateStats();
AvatarInputs::getInstance()->update();
@ -2946,9 +2944,9 @@ void Application::updateMyAvatarLookAtPosition() {
} else {
// I am not looking at anyone else, so just look forward
if (isHMD) {
glm::mat4 headPose = _avatarUpdate->getHeadPose() ;
glm::mat4 headPose = _avatarUpdate->getHeadPose();
glm::quat headRotation = glm::quat_cast(headPose);
lookAtSpot = _myCamera.getPosition() +
lookAtSpot = myAvatar->getPosition() +
myAvatar->getOrientation() * (headRotation * glm::vec3(0.0f, 0.0f, -TREE_SCALE));
} else {
lookAtSpot = myAvatar->getHead()->getEyePosition() +
@ -3055,7 +3053,7 @@ void Application::reloadResourceCaches() {
_viewFrustum.setOrientation(glm::quat());
queryOctree(NodeType::EntityServer, PacketType::EntityQuery, _entityServerJurisdictions);
emptyLocalCache();
DependencyManager::get<AssetClient>()->clearCache();
DependencyManager::get<AnimationCache>()->refreshAll();
DependencyManager::get<ModelCache>()->refreshAll();
@ -3818,7 +3816,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se
// Setup the current Zone Entity lighting
{
auto stage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
DependencyManager::get<DeferredLightingEffect>()->setGlobalLight(stage->getSunLight(), stage->getSkybox()->getCubemap());
DependencyManager::get<DeferredLightingEffect>()->setGlobalLight(stage->getSunLight());
}
{
@ -3954,37 +3952,13 @@ void Application::clearDomainOctreeDetails() {
void Application::domainChanged(const QString& domainHostname) {
updateWindowTitle();
clearDomainOctreeDetails();
_domainConnectionRefusals.clear();
// disable physics until we have enough information about our new location to not cause craziness.
_physicsEnabled = false;
}
void Application::handleDomainConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message) {
// Read deny reason from packet
quint16 reasonSize;
message->readPrimitive(&reasonSize);
QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize));
// output to the log so the user knows they got a denied connection request
// and check and signal for an access token so that we can make sure they are logged in
qCDebug(interfaceapp) << "The domain-server denied a connection request: " << reason;
qCDebug(interfaceapp) << "You may need to re-log to generate a keypair so you can provide a username signature.";
if (!_domainConnectionRefusals.contains(reason)) {
_domainConnectionRefusals.append(reason);
emit domainConnectionRefused(reason);
}
AccountManager::getInstance().checkAndSignalForAccessToken();
}
void Application::connectedToDomain(const QString& hostname) {
AccountManager& accountManager = AccountManager::getInstance();
const QUuid& domainID = DependencyManager::get<NodeList>()->getDomainHandler().getUUID();
if (accountManager.isLoggedIn() && !domainID.isNull()) {
_notifiedPacketVersionMismatchThisDomain = false;
}
void Application::resettingDomain() {
_notifiedPacketVersionMismatchThisDomain = false;
}
void Application::nodeAdded(SharedNodePointer node) {
@ -4521,33 +4495,6 @@ void Application::openUrl(const QUrl& url) {
}
}
void Application::domainSettingsReceived(const QJsonObject& domainSettingsObject) {
// from the domain-handler, figure out the satoshi cost per voxel and per meter cubed
const QString VOXEL_SETTINGS_KEY = "voxels";
const QString PER_VOXEL_COST_KEY = "per-voxel-credits";
const QString PER_METER_CUBED_COST_KEY = "per-meter-cubed-credits";
const QString VOXEL_WALLET_UUID = "voxel-wallet";
const QJsonObject& voxelObject = domainSettingsObject[VOXEL_SETTINGS_KEY].toObject();
qint64 satoshisPerVoxel = 0;
qint64 satoshisPerMeterCubed = 0;
QUuid voxelWalletUUID;
if (!domainSettingsObject.isEmpty()) {
float perVoxelCredits = (float) voxelObject[PER_VOXEL_COST_KEY].toDouble();
float perMeterCubedCredits = (float) voxelObject[PER_METER_CUBED_COST_KEY].toDouble();
satoshisPerVoxel = (qint64) floorf(perVoxelCredits * SATOSHIS_PER_CREDIT);
satoshisPerMeterCubed = (qint64) floorf(perMeterCubedCredits * SATOSHIS_PER_CREDIT);
voxelWalletUUID = QUuid(voxelObject[VOXEL_WALLET_UUID].toString());
}
qCDebug(interfaceapp) << "Octree edits costs are" << satoshisPerVoxel << "per octree cell and" << satoshisPerMeterCubed << "per meter cubed";
qCDebug(interfaceapp) << "Destination wallet UUID for edit payments is" << voxelWalletUUID;
}
void Application::loadDialog() {
auto scriptEngines = DependencyManager::get<ScriptEngines>();
QString fileNameString = OffscreenUi::getOpenFileName(

View file

@ -120,6 +120,8 @@ public:
QSize getDeviceSize() const;
bool hasFocus() const;
void showCursor(const QCursor& cursor);
bool isThrottleRendering() const;
Camera* getCamera() { return &_myCamera; }
@ -225,7 +227,6 @@ signals:
void svoImportRequested(const QString& url);
void checkBackgroundDownloads();
void domainConnectionRefused(const QString& reason);
void fullAvatarURLChanged(const QString& newValue, const QString& modelName);
@ -288,16 +289,13 @@ private slots:
void idle(uint64_t now);
void aboutToQuit();
void connectedToDomain(const QString& hostname);
void resettingDomain();
void audioMuteToggled();
void faceTrackerMuteToggled();
void activeChanged(Qt::ApplicationState state);
void domainSettingsReceived(const QJsonObject& domainSettingsObject);
void handleDomainConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message);
void notifyPacketVersionMismatch();
void loadSettings();
@ -328,8 +326,6 @@ private:
void cleanupBeforeQuit();
void emptyLocalCache();
void update(float deltaTime);
void setPalmData(Hand* hand, const controller::Pose& pose, float deltaTime, HandData::Hand whichHand, float triggerValue);
@ -476,7 +472,6 @@ private:
typedef bool (Application::* AcceptURLMethod)(const QString &);
static const QHash<QString, AcceptURLMethod> _acceptedExtensions;
QList<QString> _domainConnectionRefusals;
glm::uvec2 _renderResolution;
int _maxOctreePPS = DEFAULT_MAX_OCTREE_PPS;
@ -515,6 +510,11 @@ private:
QTimer* _idleTimer { nullptr };
bool _fakedMouseEvent { false };
void checkChangeCursor();
mutable QMutex _changeCursorLock { QMutex::Recursive };
QCursor _desiredCursor{ Qt::BlankCursor };
bool _cursorNeedsChanging { false };
};
#endif // hifi_Application_h

View file

@ -22,11 +22,8 @@
#include <QStandardPaths>
#include <QVBoxLayout>
#include "DataServerAccountInfo.h"
#include "Menu.h"
Q_DECLARE_METATYPE(DataServerAccountInfo)
static const QString RUNNING_MARKER_FILENAME = "Interface.running";
void CrashHandler::checkForAndHandleCrash() {
@ -57,7 +54,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() {
layout->addWidget(label);
QRadioButton* option1 = new QRadioButton("Reset all my settings");
QRadioButton* option2 = new QRadioButton("Reset my settings but retain login and avatar info.");
QRadioButton* option2 = new QRadioButton("Reset my settings but retain avatar info.");
QRadioButton* option3 = new QRadioButton("Continue with my current settings");
option3->setChecked(true);
layout->addWidget(option1);
@ -79,7 +76,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() {
return CrashHandler::DELETE_INTERFACE_INI;
}
if (option2->isChecked()) {
return CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO;
return CrashHandler::RETAIN_AVATAR_INFO;
}
}
@ -88,7 +85,7 @@ CrashHandler::Action CrashHandler::promptUserForAction() {
}
void CrashHandler::handleCrash(CrashHandler::Action action) {
if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
if (action != CrashHandler::DELETE_INTERFACE_INI && action != CrashHandler::RETAIN_AVATAR_INFO) {
// CrashHandler::DO_NOTHING or unexpected value
return;
}
@ -101,18 +98,13 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
const QString DISPLAY_NAME_KEY = "displayName";
const QString FULL_AVATAR_URL_KEY = "fullAvatarURL";
const QString FULL_AVATAR_MODEL_NAME_KEY = "fullAvatarModelName";
const QString ACCOUNTS_GROUP = "accounts";
QString displayName;
QUrl fullAvatarURL;
QString fullAvatarModelName;
QUrl address;
QMap<QString, DataServerAccountInfo> accounts;
if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
// Read login and avatar info
qRegisterMetaType<DataServerAccountInfo>("DataServerAccountInfo");
qRegisterMetaTypeStreamOperators<DataServerAccountInfo>("DataServerAccountInfo");
if (action == CrashHandler::RETAIN_AVATAR_INFO) {
// Read avatar info
// Location and orientation
settings.beginGroup(ADDRESS_MANAGER_GROUP);
@ -125,13 +117,6 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
fullAvatarURL = settings.value(FULL_AVATAR_URL_KEY).toUrl();
fullAvatarModelName = settings.value(FULL_AVATAR_MODEL_NAME_KEY).toString();
settings.endGroup();
// Accounts
settings.beginGroup(ACCOUNTS_GROUP);
foreach(const QString& key, settings.allKeys()) {
accounts.insert(key, settings.value(key).value<DataServerAccountInfo>());
}
settings.endGroup();
}
// Delete Interface.ini
@ -140,8 +125,8 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
settingsFile.remove();
}
if (action == CrashHandler::RETAIN_LOGIN_AND_AVATAR_INFO) {
// Write login and avatar info
if (action == CrashHandler::RETAIN_AVATAR_INFO) {
// Write avatar info
// Location and orientation
settings.beginGroup(ADDRESS_MANAGER_GROUP);
@ -154,13 +139,6 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
settings.setValue(FULL_AVATAR_URL_KEY, fullAvatarURL);
settings.setValue(FULL_AVATAR_MODEL_NAME_KEY, fullAvatarModelName);
settings.endGroup();
// Accounts
settings.beginGroup(ACCOUNTS_GROUP);
foreach(const QString& key, accounts.keys()) {
settings.setValue(key, QVariant::fromValue(accounts.value(key)));
}
settings.endGroup();
}
}

View file

@ -25,7 +25,7 @@ public:
private:
enum Action {
DELETE_INTERFACE_INI,
RETAIN_LOGIN_AND_AVATAR_INFO,
RETAIN_AVATAR_INFO,
DO_NOTHING
};

View file

@ -31,7 +31,6 @@ MainWindow::MainWindow(QWidget* parent) :
_windowState("WindowState", 0)
{
setAcceptDrops(true);
_trayIcon.show();
}
void MainWindow::restoreGeometry() {

View file

@ -13,7 +13,6 @@
#define __hifi__MainWindow__
#include <QMainWindow>
#include <QSystemTrayIcon>
#include <SettingHandle.h>
@ -43,7 +42,6 @@ protected:
private:
Setting::Handle<QRect> _windowGeometry;
Setting::Handle<int> _windowState;
QSystemTrayIcon _trayIcon;
};
#endif /* defined(__hifi__MainWindow__) */

View file

@ -342,9 +342,9 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename
void ModelPackager::listTextures() {
_textures.clear();
foreach (const FBXMaterial mat, _geometry->materials) {
if (!mat.diffuseTexture.filename.isEmpty() && mat.diffuseTexture.content.isEmpty() &&
!_textures.contains(mat.diffuseTexture.filename)) {
_textures << mat.diffuseTexture.filename;
if (!mat.albedoTexture.filename.isEmpty() && mat.albedoTexture.content.isEmpty() &&
!_textures.contains(mat.albedoTexture.filename)) {
_textures << mat.albedoTexture.filename;
}
if (!mat.normalTexture.filename.isEmpty() && mat.normalTexture.content.isEmpty() &&
!_textures.contains(mat.normalTexture.filename)) {

View file

@ -18,8 +18,6 @@
#include "ModelSelector.h"
static const QString AVATAR_HEAD_STRING = "Avatar Head Only";
static const QString AVATAR_BODY_STRING = "Avatar Body Only";
static const QString AVATAR_HEAD_AND_BODY_STRING = "Avatar Body with Head";
static const QString AVATAR_ATTACHEMENT_STRING = "Avatar Attachment";
static const QString ENTITY_MODEL_STRING = "Entity Model";
@ -35,8 +33,7 @@ ModelSelector::ModelSelector() {
form->addRow("Model File:", _browseButton);
_modelType = new QComboBox(this);
_modelType->addItem(AVATAR_HEAD_STRING);
_modelType->addItem(AVATAR_BODY_STRING);
_modelType->addItem(AVATAR_HEAD_AND_BODY_STRING);
_modelType->addItem(AVATAR_ATTACHEMENT_STRING);
_modelType->addItem(ENTITY_MODEL_STRING);
@ -55,11 +52,7 @@ QFileInfo ModelSelector::getFileInfo() const {
FSTReader::ModelType ModelSelector::getModelType() const {
QString text = _modelType->currentText();
if (text == AVATAR_HEAD_STRING) {
return FSTReader::HEAD_MODEL;
} else if (text == AVATAR_BODY_STRING) {
return FSTReader::BODY_ONLY_MODEL;
} else if (text == AVATAR_HEAD_AND_BODY_STRING) {
if (text == AVATAR_HEAD_AND_BODY_STRING) {
return FSTReader::HEAD_AND_BODY_MODEL;
} else if (text == AVATAR_ATTACHEMENT_STRING) {
return FSTReader::ATTACHMENT_MODEL;

View file

@ -400,7 +400,7 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
frustum = qApp->getDisplayViewFrustum();
}
if (frustum->sphereIntersectsFrustum(getPosition(), boundingRadius)) {
if (!frustum->sphereIntersectsFrustum(getPosition(), boundingRadius)) {
endRender();
return;
}

View file

@ -153,25 +153,6 @@ std::shared_ptr<Avatar> AvatarActionHold::getTarget(float deltaTimeStep, glm::qu
palmPosition = holdingAvatar->getLeftPalmPosition();
palmRotation = holdingAvatar->getLeftPalmRotation();
}
// In this case we are simulating the grab of another avatar.
// Because the hand controller velocity for their palms is not transmitted over the
// network, we have to synthesize our own.
if (_previousSet) {
// smooth linear velocity over two frames
glm::vec3 positionalDelta = palmPosition - _previousPositionalTarget;
linearVelocity = (positionalDelta + _previousPositionalDelta) / (deltaTimeStep + _previousDeltaTimeStep);
glm::quat deltaRotation = palmRotation * glm::inverse(_previousRotationalTarget);
float rotationAngle = glm::angle(deltaRotation);
if (rotationAngle > EPSILON) {
angularVelocity = glm::normalize(glm::axis(deltaRotation));
angularVelocity *= (rotationAngle / deltaTimeStep);
}
_previousPositionalDelta = positionalDelta;
_previousDeltaTimeStep = deltaTimeStep;
}
}
rotation = palmRotation * _relativeRotation;
@ -278,6 +259,7 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) {
});
forceBodyNonStatic();
activateBody(true);
}
bool AvatarActionHold::updateArguments(QVariantMap arguments) {

View file

@ -31,7 +31,14 @@ void AvatarUpdate::synchronousProcess() {
// Keep our own updated value, so that our asynchronous code can consult it.
_isHMDMode = qApp->isHMDMode();
auto frameCount = qApp->getFrameCount();
_headPose = qApp->getActiveDisplayPlugin()->getHeadPose(frameCount);
QSharedPointer<AvatarManager> manager = DependencyManager::get<AvatarManager>();
MyAvatar* myAvatar = manager->getMyAvatar();
assert(myAvatar);
// transform the head pose from the displayPlugin into avatar coordinates.
glm::mat4 invAvatarMat = glm::inverse(createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()));
_headPose = invAvatarMat * (myAvatar->getSensorToWorldMatrix() * qApp->getActiveDisplayPlugin()->getHeadPose(frameCount));
if (!isThreaded()) {
process();

View file

@ -19,6 +19,7 @@
#include <QtCore/QTimer>
#include <scripting/HMDScriptingInterface.h>
#include <AccountManager.h>
#include <AddressManager.h>
#include <AudioClient.h>
@ -849,7 +850,7 @@ void MyAvatar::updateLookAtTargetAvatar() {
avatar->setIsLookAtTarget(false);
if (!avatar->isMyAvatar() && avatar->isInitialized() &&
(distanceTo < GREATEST_LOOKING_AT_DISTANCE * getUniformScale())) {
float angleTo = glm::angle(lookForward, glm::normalize(avatar->getHead()->getEyePosition() - cameraPosition));
float angleTo = glm::angle(lookForward, glm::normalize(avatar->getHead()->getEyePosition() - getHead()->getEyePosition()));
if (angleTo < (smallestAngleTo * (isCurrentTarget ? KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR : 1.0f))) {
_lookAtTargetAvatar = avatarPointer;
_targetAvatarPosition = avatarPointer->getPosition();
@ -864,36 +865,29 @@ void MyAvatar::updateLookAtTargetAvatar() {
// Let's get everything to world space:
glm::vec3 avatarLeftEye = getHead()->getLeftEyePosition();
glm::vec3 avatarRightEye = getHead()->getRightEyePosition();
// When not in HMD, these might both answer identity (i.e., the bridge of the nose). That's ok.
// By my inpsection of the code and live testing, getEyeOffset and getEyePose are the same. (Application hands identity as offset matrix.)
// This might be more work than needed for any given use, but as we explore different formulations, we go mad if we don't work in world space.
glm::mat4 leftEye = qApp->getEyeOffset(Eye::Left);
glm::mat4 rightEye = qApp->getEyeOffset(Eye::Right);
glm::vec3 leftEyeHeadLocal = glm::vec3(leftEye[3]);
glm::vec3 rightEyeHeadLocal = glm::vec3(rightEye[3]);
auto humanSystem = qApp->getViewFrustum();
glm::vec3 humanLeftEye = humanSystem->getPosition() + (humanSystem->getOrientation() * leftEyeHeadLocal);
glm::vec3 humanRightEye = humanSystem->getPosition() + (humanSystem->getOrientation() * rightEyeHeadLocal);
// First find out where (in world space) the person is looking relative to that bridge-of-the-avatar point.
// (We will be adding that offset to the camera position, after making some other adjustments.)
glm::vec3 gazeOffset = lookAtPosition - getHead()->getEyePosition();
// Scale by proportional differences between avatar and human.
float humanEyeSeparationInModelSpace = glm::length(humanLeftEye - humanRightEye);
float avatarEyeSeparation = glm::length(avatarLeftEye - avatarRightEye);
gazeOffset = gazeOffset * humanEyeSeparationInModelSpace / avatarEyeSeparation;
// scale gazeOffset by IPD, if wearing an HMD.
if (qApp->isHMDMode()) {
glm::mat4 leftEye = qApp->getEyeOffset(Eye::Left);
glm::mat4 rightEye = qApp->getEyeOffset(Eye::Right);
glm::vec3 leftEyeHeadLocal = glm::vec3(leftEye[3]);
glm::vec3 rightEyeHeadLocal = glm::vec3(rightEye[3]);
auto humanSystem = qApp->getViewFrustum();
glm::vec3 humanLeftEye = humanSystem->getPosition() + (humanSystem->getOrientation() * leftEyeHeadLocal);
glm::vec3 humanRightEye = humanSystem->getPosition() + (humanSystem->getOrientation() * rightEyeHeadLocal);
// If the camera is also not oriented with the head, adjust by getting the offset in head-space...
/* Not needed (i.e., code is a no-op), but I'm leaving the example code here in case something like this is needed someday.
glm::quat avatarHeadOrientation = getHead()->getOrientation();
glm::vec3 gazeOffsetLocalToHead = glm::inverse(avatarHeadOrientation) * gazeOffset;
// ... and treat that as though it were in camera space, bringing it back to world space.
// But camera is fudged to make the picture feel like the avatar's orientation.
glm::quat humanOrientation = humanSystem->getOrientation(); // or just avatar getOrienation() ?
gazeOffset = humanOrientation * gazeOffsetLocalToHead;
glm::vec3 corrected = humanSystem->getPosition() + gazeOffset;
*/
auto hmdInterface = DependencyManager::get<HMDScriptingInterface>();
float ipdScale = hmdInterface->getIPDScale();
// Scale by proportional differences between avatar and human.
float humanEyeSeparationInModelSpace = glm::length(humanLeftEye - humanRightEye) * ipdScale;
float avatarEyeSeparation = glm::length(avatarLeftEye - avatarRightEye);
gazeOffset = gazeOffset * humanEyeSeparationInModelSpace / avatarEyeSeparation;
}
// And now we can finally add that offset to the camera.
glm::vec3 corrected = qApp->getViewFrustum()->getPosition() + gazeOffset;
@ -1097,7 +1091,8 @@ void MyAvatar::prepareForPhysicsSimulation() {
_characterController.setTargetVelocity(getTargetVelocity());
_characterController.setPositionAndOrientation(getPosition(), getOrientation());
if (qApp->isHMDMode()) {
_follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix);
bool hasDriveInput = fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f;
_follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput);
} else {
_follow.deactivate();
}
@ -1339,11 +1334,11 @@ void MyAvatar::preRender(RenderArgs* renderArgs) {
_prevShouldDrawHead = shouldDrawHead;
}
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.6f;
const float RENDER_HEAD_CUTOFF_DISTANCE = 0.3f;
bool MyAvatar::cameraInsideHead() const {
const glm::vec3 cameraPosition = qApp->getCamera()->getPosition();
return glm::length(cameraPosition - getDefaultEyePosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale());
return glm::length(cameraPosition - getHeadPosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getUniformScale());
}
bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const {
@ -1776,25 +1771,6 @@ glm::quat MyAvatar::getWorldBodyOrientation() const {
return glm::quat_cast(_sensorToWorldMatrix * _bodySensorMatrix);
}
#if 0
// derive avatar body position and orientation from the current HMD Sensor location.
// results are in sensor space
glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const {
if (_rig) {
// orientation
const glm::quat hmdOrientation = getHMDSensorOrientation();
const glm::quat yaw = cancelOutRollAndPitch(hmdOrientation);
// position
// we flip about yAxis when going from "root" to "avatar" frame
// and we must also apply "yaw" to get into HMD frame
glm::quat rotY180 = glm::angleAxis((float)M_PI, glm::vec3(0.0f, 1.0f, 0.0f));
glm::vec3 eyesInAvatarFrame = rotY180 * yaw * _rig->getEyesInRootFrame();
glm::vec3 bodyPos = getHMDSensorPosition() - eyesInAvatarFrame;
return createMatFromQuatAndPos(yaw, bodyPos);
}
return glm::mat4();
}
#else
// old school meat hook style
glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const {
@ -1835,7 +1811,6 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const {
return createMatFromQuatAndPos(hmdOrientationYawOnly, bodyPos);
}
#endif
glm::vec3 MyAvatar::getPositionForAudio() {
switch (_audioListenerMode) {
@ -1884,58 +1859,132 @@ void MyAvatar::lateUpdatePalms() {
static const float FOLLOW_TIME = 0.5f;
void MyAvatar::FollowHelper::deactivate() {
_timeRemaining = 0.0f;
MyAvatar::FollowHelper::FollowHelper() {
deactivate();
}
void MyAvatar::FollowHelper::activate() {
void MyAvatar::FollowHelper::deactivate() {
for (int i = 0; i < NumFollowTypes; i++) {
deactivate((FollowType)i);
}
}
void MyAvatar::FollowHelper::deactivate(FollowType type) {
assert(type >= 0 && type < NumFollowTypes);
_timeRemaining[(int)type] = 0.0f;
}
void MyAvatar::FollowHelper::activate(FollowType type) {
assert(type >= 0 && type < NumFollowTypes);
// TODO: Perhaps, the follow time should be proportional to the displacement.
_timeRemaining = FOLLOW_TIME;
_timeRemaining[(int)type] = FOLLOW_TIME;
}
bool MyAvatar::FollowHelper::isActive(FollowType type) const {
assert(type >= 0 && type < NumFollowTypes);
return _timeRemaining[(int)type] > 0.0f;
}
bool MyAvatar::FollowHelper::isActive() const {
return _timeRemaining > 0.0f;
for (int i = 0; i < NumFollowTypes; i++) {
if (isActive((FollowType)i)) {
return true;
}
}
return false;
}
bool MyAvatar::FollowHelper::shouldActivate(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
float MyAvatar::FollowHelper::getMaxTimeRemaining() const {
float max = 0.0f;
for (int i = 0; i < NumFollowTypes; i++) {
if (_timeRemaining[i] > max) {
max = _timeRemaining[i];
}
}
return max;
}
const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 4.0f);
void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) {
for (int i = 0; i < NumFollowTypes; i++) {
_timeRemaining[i] -= dt;
}
}
bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees
glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix);
if (glm::dot(myAvatar.getHMDSensorFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD) {
return glm::dot(myAvatar.getHMDSensorFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD;
}
bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
// -z axis of currentBodyMatrix in world space.
glm::vec3 forward = glm::normalize(glm::vec3(-currentBodyMatrix[0][2], -currentBodyMatrix[1][2], -currentBodyMatrix[2][2]));
// x axis of currentBodyMatrix in world space.
glm::vec3 right = glm::normalize(glm::vec3(currentBodyMatrix[0][0], currentBodyMatrix[1][0], currentBodyMatrix[2][0]));
glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix);
float forwardLeanAmount = glm::dot(forward, offset);
float lateralLeanAmount = glm::dot(right, offset);
const float MAX_LATERAL_LEAN = 0.3f;
const float MAX_FORWARD_LEAN = 0.15f;
const float MAX_BACKWARD_LEAN = 0.1f;
if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) {
return true;
} else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) {
return true;
}
const float CYLINDER_TOP = 0.1f;
const float CYLINDER_BOTTOM = -0.5f;
const float CYLINDER_RADIUS = 0.15f;
glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix);
glm::vec3 radialOffset(offset.x, 0.0f, offset.z);
float radialDistance = glm::length(radialOffset);
return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM) || (radialDistance > CYLINDER_RADIUS);
return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN;
}
void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) {
bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const {
const float CYLINDER_TOP = 0.1f;
const float CYLINDER_BOTTOM = -1.5f;
glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix);
return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM);
}
void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput) {
_desiredBodyMatrix = desiredBodyMatrix;
if (!isActive() && shouldActivate(myAvatar, desiredBodyMatrix, currentBodyMatrix)) {
activate();
if (!isActive(Rotation) && shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix)) {
activate(Rotation);
}
if (!isActive(Horizontal) && shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix)) {
activate(Horizontal);
}
if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) {
activate(Vertical);
}
if (isActive()) {
glm::mat4 desiredWorldMatrix = myAvatar.getSensorToWorldMatrix() * _desiredBodyMatrix;
myAvatar.getCharacterController()->setFollowParameters(desiredWorldMatrix, _timeRemaining);
} else {
glm::mat4 currentWorldMatrix = myAvatar.getSensorToWorldMatrix() * currentBodyMatrix;
myAvatar.getCharacterController()->setFollowParameters(currentWorldMatrix, 0.0f);
glm::mat4 desiredWorldMatrix = myAvatar.getSensorToWorldMatrix() * _desiredBodyMatrix;
glm::mat4 currentWorldMatrix = myAvatar.getSensorToWorldMatrix() * currentBodyMatrix;
AnimPose followWorldPose(currentWorldMatrix);
if (isActive(Rotation)) {
followWorldPose.rot = glmExtractRotation(desiredWorldMatrix);
}
if (isActive(Horizontal)) {
glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix);
followWorldPose.trans.x = desiredTranslation.x;
followWorldPose.trans.z = desiredTranslation.z;
}
if (isActive(Vertical)) {
glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix);
followWorldPose.trans.y = desiredTranslation.y;
}
myAvatar.getCharacterController()->setFollowParameters(followWorldPose, getMaxTimeRemaining());
}
glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix) {
if (isActive()) {
float dt = myAvatar.getCharacterController()->getFollowTime();
_timeRemaining -= dt;
decrementTimeRemaining(dt);
// apply follow displacement to the body matrix.
glm::vec3 worldLinearDisplacement = myAvatar.getCharacterController()->getFollowLinearDisplacement();

View file

@ -392,14 +392,29 @@ private:
glm::mat4 _sensorToWorldMatrix;
struct FollowHelper {
FollowHelper();
enum FollowType {
Rotation = 0,
Horizontal,
Vertical,
NumFollowTypes
};
glm::mat4 _desiredBodyMatrix;
float _timeRemaining { 0.0f };
float _timeRemaining[NumFollowTypes];
void deactivate();
void deactivate(FollowType type);
void activate();
void activate(FollowType type);
bool isActive() const;
bool shouldActivate(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix);
bool isActive(FollowType followType) const;
float getMaxTimeRemaining() const;
void decrementTimeRemaining(float dt);
bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const;
void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput);
glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix);
};
FollowHelper _follow;

View file

@ -25,7 +25,7 @@
WindowScriptingInterface::WindowScriptingInterface() {
const DomainHandler& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &WindowScriptingInterface::domainChanged);
connect(qApp, &Application::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused);
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &WindowScriptingInterface::domainConnectionRefused);
connect(qApp, &Application::svoImportRequested, [this](const QString& urlString) {
static const QMetaMethod svoImportRequestedSignal =

Some files were not shown because too many files have changed in this diff Show more