mirror of
https://github.com/lubosz/overte.git
synced 2025-04-27 06:55:40 +02:00
Merge branch 'master' into 20769
This commit is contained in:
commit
11af0a421b
846 changed files with 39517 additions and 16574 deletions
README.md
assignment-client/src
Agent.cppAssignmentClientApp.cppAssignmentClientMonitor.cppAssignmentClientMonitor.h
assets
audio
entities
octree
cmake
externals
macros
AutoScribeShader.cmakeGenerateInstallers.cmakeInstallBesideConsole.cmakeLinkHifiLibraries.cmakeManuallyInstallOpenSSLForQt.cmakeManuallyInstallSSLEay.cmakeSetPackagingParameters.cmake
modules
templates
domain-server
resources
src
examples
acScripts
airship
attachedEntitiesManager.jsaudioExamples/acAudioSearching
away.jscontrollers
cows
data_visualization
defaultScripts.jsdepthReticle.jsdirectory.jsedit.jsentityScripts
fireflies
grab.jsgridTest.jshmdControls.jshmdDefaults.jshomeContent/whiteboardV2
html
edit-style.cssentityList.htmlentityProperties.htmleventBridgeLoader.jsgridControls.htmlqmlWebTest.htmlstyle.css
libraries
marketplace.jsparticle_explorer
playTestSound.jsplaya/fireworks
selectAudioDevice.jsshaders
tests
avatarAttachmentTest.js
basicEntityTest
batonSoundEntityTest
cube_texture.pngmat4test.jsparticleOrientationTest.jsqmlWebTest.jsrapidProceduralChange
|
@ -8,14 +8,14 @@ like to get paid for your work, make sure you report the bug via a job on
|
|||
[Worklist.net](https://worklist.net).
|
||||
|
||||
We're hiring! We're looking for skilled developers;
|
||||
send your resume to hiring@highfidelity.io
|
||||
send your resume to hiring@highfidelity.com
|
||||
|
||||
##### Chat with us
|
||||
Come chat with us in [our Gitter](http://gitter.im/highfidelity/hifi) if you have any questions or just want to say hi!
|
||||
|
||||
Documentation
|
||||
=========
|
||||
Documentation is available at [docs.highfidelity.io](http://docs.highfidelity.io), if something is missing, please suggest it via a new job on Worklist (add to the hifi-docs project).
|
||||
Documentation is available at [docs.highfidelity.com](http://docs.highfidelity.com), if something is missing, please suggest it via a new job on Worklist (add to the hifi-docs project).
|
||||
|
||||
Build Instructions
|
||||
=========
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
@ -228,7 +222,6 @@ void Agent::executeScript() {
|
|||
scriptedAvatar->setForceFaceTrackerConnected(true);
|
||||
|
||||
// call model URL setters with empty URLs so our avatar, if user, will have the default models
|
||||
scriptedAvatar->setFaceModelURL(QUrl());
|
||||
scriptedAvatar->setSkeletonModelURL(QUrl());
|
||||
|
||||
// give this AvatarData object to the script engine
|
||||
|
@ -471,11 +464,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>();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,19 +12,21 @@
|
|||
|
||||
#include "AssetServer.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QString>
|
||||
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
#include "NetworkLogging.h"
|
||||
#include "NodeType.h"
|
||||
#include "SendAssetTask.h"
|
||||
#include "UploadAssetTask.h"
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
|
||||
|
||||
|
@ -42,6 +44,7 @@ AssetServer::AssetServer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet");
|
||||
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
|
||||
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
|
||||
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
|
||||
}
|
||||
|
||||
void AssetServer::run() {
|
||||
|
@ -56,6 +59,8 @@ void AssetServer::run() {
|
|||
ThreadedAssignment::commonInit(ASSET_SERVER_LOGGING_TARGET_NAME, NodeType::AssetServer);
|
||||
}
|
||||
|
||||
static const QString ASSET_FILES_SUBDIR = "files";
|
||||
|
||||
void AssetServer::completeSetup() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
|
@ -72,6 +77,18 @@ void AssetServer::completeSetup() {
|
|||
|
||||
auto assetServerObject = settingsObject[ASSET_SERVER_SETTINGS_KEY].toObject();
|
||||
|
||||
static const QString MAX_BANDWIDTH_OPTION = "max_bandwidth";
|
||||
auto maxBandwidthValue = assetServerObject[MAX_BANDWIDTH_OPTION];
|
||||
auto maxBandwidthFloat = maxBandwidthValue.toDouble(-1);
|
||||
|
||||
if (maxBandwidthFloat > 0.0) {
|
||||
const int BITS_PER_MEGABITS = 1000 * 1000;
|
||||
int maxBandwidth = maxBandwidthFloat * BITS_PER_MEGABITS;
|
||||
nodeList->setConnectionMaxBandwidth(maxBandwidth);
|
||||
qInfo() << "Set maximum bandwith per connection to" << maxBandwidthFloat << "Mb/s."
|
||||
" (" << maxBandwidth << "bits/s)";
|
||||
}
|
||||
|
||||
// get the path to the asset folder from the domain server settings
|
||||
static const QString ASSETS_PATH_OPTION = "assets_path";
|
||||
auto assetsJSONValue = assetServerObject[ASSETS_PATH_OPTION];
|
||||
|
@ -96,85 +113,208 @@ void AssetServer::completeSetup() {
|
|||
|
||||
qDebug() << "Creating resources directory";
|
||||
_resourcesDirectory.mkpath(".");
|
||||
_filesDirectory = _resourcesDirectory;
|
||||
|
||||
bool noExistingAssets = !_resourcesDirectory.exists() || _resourcesDirectory.entryList(QDir::Files).size() == 0;
|
||||
|
||||
if (noExistingAssets) {
|
||||
qDebug() << "Asset resources directory empty, searching for existing asset resources to migrate";
|
||||
QString oldDataDirectory = QCoreApplication::applicationDirPath();
|
||||
|
||||
const QString OLD_RESOURCES_PATH = "assets";
|
||||
|
||||
auto oldResourcesDirectory = QDir(oldDataDirectory).filePath("resources/" + OLD_RESOURCES_PATH);
|
||||
|
||||
|
||||
if (QDir(oldResourcesDirectory).exists()) {
|
||||
qDebug() << "Existing assets found in " << oldResourcesDirectory << ", copying to " << _resourcesDirectory;
|
||||
|
||||
|
||||
QDir resourcesParentDirectory = _resourcesDirectory.filePath("..");
|
||||
if (!resourcesParentDirectory.exists()) {
|
||||
qDebug() << "Creating data directory " << resourcesParentDirectory.absolutePath();
|
||||
resourcesParentDirectory.mkpath(".");
|
||||
}
|
||||
|
||||
auto files = QDir(oldResourcesDirectory).entryList(QDir::Files);
|
||||
|
||||
for (auto& file : files) {
|
||||
auto from = oldResourcesDirectory + QDir::separator() + file;
|
||||
auto to = _resourcesDirectory.absoluteFilePath(file);
|
||||
qDebug() << "\tCopying from " << from << " to " << to;
|
||||
QFile::copy(from, to);
|
||||
}
|
||||
|
||||
}
|
||||
if (!_resourcesDirectory.mkpath(ASSET_FILES_SUBDIR) || !_filesDirectory.cd(ASSET_FILES_SUBDIR)) {
|
||||
qCritical() << "Unable to create file directory for asset-server files. Stopping assignment.";
|
||||
setFinished(true);
|
||||
return;
|
||||
}
|
||||
qDebug() << "Serving files from: " << _resourcesDirectory.path();
|
||||
|
||||
// Scan for new files
|
||||
qDebug() << "Looking for new files in asset directory";
|
||||
auto files = _resourcesDirectory.entryInfoList(QDir::Files);
|
||||
QRegExp filenameRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}(\\..+)?$" };
|
||||
for (const auto& fileInfo : files) {
|
||||
auto filename = fileInfo.fileName();
|
||||
if (!filenameRegex.exactMatch(filename)) {
|
||||
qDebug() << "Found file: " << filename;
|
||||
if (!fileInfo.isReadable()) {
|
||||
qDebug() << "\tCan't open file for reading: " << filename;
|
||||
continue;
|
||||
}
|
||||
// load whatever mappings we currently have from the local file
|
||||
loadMappingsFromFile();
|
||||
|
||||
// Read file
|
||||
QFile file { fileInfo.absoluteFilePath() };
|
||||
file.open(QFile::ReadOnly);
|
||||
QByteArray data = file.readAll();
|
||||
qInfo() << "Serving files from: " << _filesDirectory.path();
|
||||
|
||||
auto hash = hashData(data);
|
||||
auto hexHash = hash.toHex();
|
||||
// Check the asset directory to output some information about what we have
|
||||
auto files = _filesDirectory.entryList(QDir::Files);
|
||||
|
||||
qDebug() << "\tMoving " << filename << " to " << hexHash;
|
||||
|
||||
file.rename(_resourcesDirectory.absoluteFilePath(hexHash) + "." + fileInfo.suffix());
|
||||
}
|
||||
}
|
||||
QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING };
|
||||
auto hashedFiles = files.filter(hashFileRegex);
|
||||
|
||||
qInfo() << "There are" << hashedFiles.size() << "asset files in the asset directory.";
|
||||
|
||||
performMappingMigration();
|
||||
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
}
|
||||
|
||||
void AssetServer::performMappingMigration() {
|
||||
QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}(\\.[\\w]+)+$" };
|
||||
|
||||
auto files = _resourcesDirectory.entryInfoList(QDir::Files);
|
||||
|
||||
for (const auto& fileInfo : files) {
|
||||
if (hashFileRegex.exactMatch(fileInfo.fileName())) {
|
||||
// we have a pre-mapping file that we should migrate to the new mapping system
|
||||
qDebug() << "Migrating pre-mapping file" << fileInfo.fileName();
|
||||
|
||||
// rename the file to the same name with no extension
|
||||
QFile oldFile { fileInfo.absoluteFilePath() };
|
||||
|
||||
auto oldAbsolutePath = fileInfo.absoluteFilePath();
|
||||
auto oldFilename = fileInfo.fileName();
|
||||
auto hash = oldFilename.left(SHA256_HASH_HEX_LENGTH);
|
||||
auto fullExtension = oldFilename.mid(oldFilename.indexOf('.'));
|
||||
|
||||
qDebug() << "\tMoving" << oldAbsolutePath << "to" << oldAbsolutePath.replace(fullExtension, "");
|
||||
|
||||
bool renamed = oldFile.copy(_filesDirectory.filePath(hash));
|
||||
if (!renamed) {
|
||||
qWarning() << "\tCould not migrate pre-mapping file" << fileInfo.fileName();
|
||||
} else {
|
||||
qDebug() << "\tRenamed pre-mapping file" << fileInfo.fileName();
|
||||
|
||||
// add a new mapping with the old extension and a truncated version of the hash
|
||||
const int TRUNCATED_HASH_NUM_CHAR = 16;
|
||||
auto fakeFileName = "/" + hash.left(TRUNCATED_HASH_NUM_CHAR) + fullExtension;
|
||||
|
||||
qDebug() << "\tAdding a migration mapping from" << fakeFileName << "to" << hash;
|
||||
|
||||
auto it = _fileMappings.find(fakeFileName);
|
||||
if (it == _fileMappings.end()) {
|
||||
_fileMappings[fakeFileName] = hash;
|
||||
|
||||
if (writeMappingsToFile()) {
|
||||
// mapping added and persisted, we can remove the migrated file
|
||||
oldFile.remove();
|
||||
qDebug() << "\tMigration completed for" << oldFilename;
|
||||
}
|
||||
} else {
|
||||
qDebug() << "\tCould not add migration mapping for" << hash << "since a mapping for" << fakeFileName
|
||||
<< "already exists.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
MessageID messageID;
|
||||
message->readPrimitive(&messageID);
|
||||
|
||||
AssetMappingOperationType operationType;
|
||||
message->readPrimitive(&operationType);
|
||||
|
||||
auto replyPacket = NLPacketList::create(PacketType::AssetMappingOperationReply, QByteArray(), true, true);
|
||||
replyPacket->writePrimitive(messageID);
|
||||
|
||||
switch (operationType) {
|
||||
case AssetMappingOperationType::Get: {
|
||||
handleGetMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::GetAll: {
|
||||
handleGetAllMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::Set: {
|
||||
handleSetMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::Delete: {
|
||||
handleDeleteMappingsOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::Rename: {
|
||||
handleRenameMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
|
||||
}
|
||||
|
||||
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
QString assetPath = message.readString();
|
||||
|
||||
auto it = _fileMappings.find(assetPath);
|
||||
if (it != _fileMappings.end()) {
|
||||
auto assetHash = it->toString();
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
replyPacket.write(QByteArray::fromHex(assetHash.toUtf8()));
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::AssetNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
|
||||
auto count = _fileMappings.size();
|
||||
|
||||
replyPacket.writePrimitive(count);
|
||||
|
||||
for (auto it = _fileMappings.cbegin(); it != _fileMappings.cend(); ++ it) {
|
||||
replyPacket.writeString(it.key());
|
||||
replyPacket.write(QByteArray::fromHex(it.value().toString().toUtf8()));
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
if (senderNode->getCanRez()) {
|
||||
QString assetPath = message.readString();
|
||||
|
||||
auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
|
||||
|
||||
if (setMapping(assetPath, assetHash)) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
|
||||
}
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
if (senderNode->getCanRez()) {
|
||||
int numberOfDeletedMappings { 0 };
|
||||
message.readPrimitive(&numberOfDeletedMappings);
|
||||
|
||||
QStringList mappingsToDelete;
|
||||
|
||||
for (int i = 0; i < numberOfDeletedMappings; ++i) {
|
||||
mappingsToDelete << message.readString();
|
||||
}
|
||||
|
||||
if (deleteMappings(mappingsToDelete)) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
|
||||
}
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
if (senderNode->getCanRez()) {
|
||||
QString oldPath = message.readString();
|
||||
QString newPath = message.readString();
|
||||
|
||||
if (renameMapping(oldPath, newPath)) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
|
||||
}
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
QByteArray assetHash;
|
||||
MessageID messageID;
|
||||
uint8_t extensionLength;
|
||||
|
||||
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID) + sizeof(extensionLength))) {
|
||||
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) {
|
||||
qDebug() << "ERROR bad file request";
|
||||
return;
|
||||
}
|
||||
|
||||
message->readPrimitive(&messageID);
|
||||
assetHash = message->readWithoutCopy(SHA256_HASH_LENGTH);
|
||||
message->readPrimitive(&extensionLength);
|
||||
QByteArray extension = message->read(extensionLength);
|
||||
|
||||
auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply);
|
||||
|
||||
|
@ -183,8 +323,8 @@ void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, Sh
|
|||
replyPacket->writePrimitive(messageID);
|
||||
replyPacket->write(assetHash);
|
||||
|
||||
QString fileName = QString(hexHash) + "." + extension;
|
||||
QFileInfo fileInfo { _resourcesDirectory.filePath(fileName) };
|
||||
QString fileName = QString(hexHash);
|
||||
QFileInfo fileInfo { _filesDirectory.filePath(fileName) };
|
||||
|
||||
if (fileInfo.exists() && fileInfo.isReadable()) {
|
||||
qDebug() << "Opening file: " << fileInfo.filePath();
|
||||
|
@ -201,39 +341,39 @@ void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, Sh
|
|||
|
||||
void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
||||
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + sizeof(DataOffset) + sizeof(DataOffset));
|
||||
|
||||
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset));
|
||||
|
||||
if (message->getSize() < minSize) {
|
||||
qDebug() << "ERROR bad file request";
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue task
|
||||
auto task = new SendAssetTask(message, senderNode, _resourcesDirectory);
|
||||
auto task = new SendAssetTask(message, senderNode, _filesDirectory);
|
||||
_taskPool.start(task);
|
||||
}
|
||||
|
||||
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
||||
|
||||
if (senderNode->getCanRez()) {
|
||||
qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
|
||||
|
||||
auto task = new UploadAssetTask(message, senderNode, _resourcesDirectory);
|
||||
|
||||
auto task = new UploadAssetTask(message, senderNode, _filesDirectory);
|
||||
_taskPool.start(task);
|
||||
} else {
|
||||
// this is a node the domain told us is not allowed to rez entities
|
||||
// for now this also means it isn't allowed to add assets
|
||||
// so return a packet with error that indicates that
|
||||
|
||||
|
||||
auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetServerError));
|
||||
|
||||
|
||||
MessageID messageID;
|
||||
message->readPrimitive(&messageID);
|
||||
|
||||
|
||||
// write the message ID and a permission denied error
|
||||
permissionErrorPacket->writePrimitive(messageID);
|
||||
permissionErrorPacket->writePrimitive(AssetServerError::PermissionDenied);
|
||||
|
||||
|
||||
// send off the packet
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
|
||||
|
@ -242,19 +382,19 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
|
|||
|
||||
void AssetServer::sendStatsPacket() {
|
||||
QJsonObject serverStats;
|
||||
|
||||
|
||||
auto stats = DependencyManager::get<NodeList>()->sampleStatsForAllConnections();
|
||||
|
||||
|
||||
for (const auto& stat : stats) {
|
||||
QJsonObject nodeStats;
|
||||
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stat.second.endTime);
|
||||
QDateTime date = QDateTime::fromMSecsSinceEpoch(endTimeMs.count());
|
||||
|
||||
|
||||
static const float USEC_PER_SEC = 1000000.0f;
|
||||
static const float MEGABITS_PER_BYTE = 8.0f / 1000000.0f; // Bytes => Mbits
|
||||
float elapsed = (float)(stat.second.endTime - stat.second.startTime).count() / USEC_PER_SEC; // sec
|
||||
float megabitsPerSecPerByte = MEGABITS_PER_BYTE / elapsed; // Bytes => Mb/s
|
||||
|
||||
|
||||
QJsonObject connectionStats;
|
||||
connectionStats["1. Last Heard"] = date.toString();
|
||||
connectionStats["2. Est. Max (P/s)"] = stat.second.estimatedBandwith;
|
||||
|
@ -264,10 +404,10 @@ void AssetServer::sendStatsPacket() {
|
|||
connectionStats["6. Up (Mb/s)"] = stat.second.sentBytes * megabitsPerSecPerByte;
|
||||
connectionStats["7. Down (Mb/s)"] = stat.second.receivedBytes * megabitsPerSecPerByte;
|
||||
nodeStats["Connection Stats"] = connectionStats;
|
||||
|
||||
|
||||
using Events = udt::ConnectionStats::Stats::Event;
|
||||
const auto& events = stat.second.events;
|
||||
|
||||
|
||||
QJsonObject upstreamStats;
|
||||
upstreamStats["1. Sent (P/s)"] = stat.second.sendRate;
|
||||
upstreamStats["2. Sent Packets"] = stat.second.sentPackets;
|
||||
|
@ -279,7 +419,7 @@ void AssetServer::sendStatsPacket() {
|
|||
upstreamStats["8. Sent ACK2"] = events[Events::SentACK2];
|
||||
upstreamStats["9. Retransmitted"] = events[Events::Retransmission];
|
||||
nodeStats["Upstream Stats"] = upstreamStats;
|
||||
|
||||
|
||||
QJsonObject downstreamStats;
|
||||
downstreamStats["1. Recvd (P/s)"] = stat.second.receiveRate;
|
||||
downstreamStats["2. Recvd Packets"] = stat.second.receivedPackets;
|
||||
|
@ -290,7 +430,7 @@ void AssetServer::sendStatsPacket() {
|
|||
downstreamStats["7. Recvd ACK2"] = events[Events::ReceivedACK2];
|
||||
downstreamStats["8. Duplicates"] = events[Events::Duplicate];
|
||||
nodeStats["Downstream Stats"] = downstreamStats;
|
||||
|
||||
|
||||
QString uuid;
|
||||
auto nodelist = DependencyManager::get<NodeList>();
|
||||
if (stat.first == nodelist->getDomainHandler().getSockAddr()) {
|
||||
|
@ -301,10 +441,274 @@ void AssetServer::sendStatsPacket() {
|
|||
uuid = uuidStringWithoutCurlyBraces(node ? node->getUUID() : QUuid());
|
||||
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
|
||||
}
|
||||
|
||||
|
||||
serverStats[uuid] = nodeStats;
|
||||
}
|
||||
|
||||
|
||||
// send off the stats packets
|
||||
ThreadedAssignment::addPacketStatsAndSendStatsPacket(serverStats);
|
||||
}
|
||||
|
||||
static const QString MAP_FILE_NAME = "map.json";
|
||||
|
||||
void AssetServer::loadMappingsFromFile() {
|
||||
|
||||
auto mapFilePath = _resourcesDirectory.absoluteFilePath(MAP_FILE_NAME);
|
||||
|
||||
QFile mapFile { mapFilePath };
|
||||
if (mapFile.exists()) {
|
||||
if (mapFile.open(QIODevice::ReadOnly)) {
|
||||
QJsonParseError error;
|
||||
|
||||
auto jsonDocument = QJsonDocument::fromJson(mapFile.readAll(), &error);
|
||||
|
||||
if (error.error == QJsonParseError::NoError) {
|
||||
_fileMappings = jsonDocument.object().toVariantHash();
|
||||
|
||||
// remove any mappings that don't match the expected format
|
||||
auto it = _fileMappings.begin();
|
||||
while (it != _fileMappings.end()) {
|
||||
bool shouldDrop = false;
|
||||
|
||||
if (!isValidPath(it.key())) {
|
||||
qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path.";
|
||||
shouldDrop = true;
|
||||
}
|
||||
|
||||
if (!isValidHash(it.value().toString())) {
|
||||
qWarning() << "Will not keep mapping for" << it.key() << "since it does not have a valid hash.";
|
||||
shouldDrop = true;
|
||||
}
|
||||
|
||||
if (shouldDrop) {
|
||||
it = _fileMappings.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
qInfo() << "Loaded" << _fileMappings.count() << "mappings from map file at" << mapFilePath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
qCritical() << "Failed to read mapping file at" << mapFilePath << "- assignment will not continue.";
|
||||
setFinished(true);
|
||||
} else {
|
||||
qInfo() << "No existing mappings loaded from file since no file was found at" << mapFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetServer::writeMappingsToFile() {
|
||||
auto mapFilePath = _resourcesDirectory.absoluteFilePath(MAP_FILE_NAME);
|
||||
|
||||
QFile mapFile { mapFilePath };
|
||||
if (mapFile.open(QIODevice::WriteOnly)) {
|
||||
auto jsonObject = QJsonObject::fromVariantHash(_fileMappings);
|
||||
QJsonDocument jsonDocument { jsonObject };
|
||||
|
||||
if (mapFile.write(jsonDocument.toJson()) != -1) {
|
||||
qDebug() << "Wrote JSON mappings to file at" << mapFilePath;
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "Failed to write JSON mappings to file at" << mapFilePath;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Failed to open map file at" << mapFilePath;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AssetServer::setMapping(AssetPath path, AssetHash hash) {
|
||||
path = path.trimmed();
|
||||
|
||||
if (!isValidPath(path)) {
|
||||
qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidHash(hash)) {
|
||||
qWarning() << "Cannot set a mapping for invalid hash" << path << "=>" << hash;
|
||||
return false;
|
||||
}
|
||||
|
||||
// remember what the old mapping was in case persistence fails
|
||||
auto oldMapping = _fileMappings.value(path).toString();
|
||||
|
||||
// update the in memory QHash
|
||||
_fileMappings[path] = hash;
|
||||
|
||||
// attempt to write to file
|
||||
if (writeMappingsToFile()) {
|
||||
// persistence succeeded, we are good to go
|
||||
qDebug() << "Set mapping:" << path << "=>" << hash;
|
||||
return true;
|
||||
} else {
|
||||
// failed to persist this mapping to file - put back the old one in our in-memory representation
|
||||
if (oldMapping.isEmpty()) {
|
||||
_fileMappings.remove(path);
|
||||
} else {
|
||||
_fileMappings[path] = oldMapping;
|
||||
}
|
||||
|
||||
qWarning() << "Failed to persist mapping:" << path << "=>" << hash;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool pathIsFolder(const AssetPath& path) {
|
||||
return path.endsWith('/');
|
||||
}
|
||||
|
||||
bool AssetServer::deleteMappings(AssetPathList& paths) {
|
||||
// take a copy of the current mappings in case persistence of these deletes fails
|
||||
auto oldMappings = _fileMappings;
|
||||
|
||||
// enumerate the paths to delete and remove them all
|
||||
for (auto& path : paths) {
|
||||
|
||||
path = path.trimmed();
|
||||
|
||||
// figure out if this path will delete a file or folder
|
||||
if (pathIsFolder(path)) {
|
||||
// enumerate the in memory file mappings and remove anything that matches
|
||||
auto it = _fileMappings.begin();
|
||||
auto sizeBefore = _fileMappings.size();
|
||||
|
||||
while (it != _fileMappings.end()) {
|
||||
if (it.key().startsWith(path)) {
|
||||
it = _fileMappings.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
auto sizeNow = _fileMappings.size();
|
||||
if (sizeBefore != sizeNow) {
|
||||
qDebug() << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path;
|
||||
} else {
|
||||
qDebug() << "Did not find any mappings to delete in folder:" << path;
|
||||
}
|
||||
|
||||
} else {
|
||||
auto oldMapping = _fileMappings.take(path);
|
||||
if (!oldMapping.isNull()) {
|
||||
qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString();
|
||||
} else {
|
||||
qDebug() << "Unable to delete a mapping that was not found:" << path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleted the old mappings, attempt to persist to file
|
||||
if (writeMappingsToFile()) {
|
||||
// persistence succeeded we are good to go
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "Failed to persist deleted mappings, rolling back";
|
||||
|
||||
// we didn't delete the previous mapping, put it back in our in-memory representation
|
||||
_fileMappings = oldMappings;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
|
||||
oldPath = oldPath.trimmed();
|
||||
newPath = newPath.trimmed();
|
||||
|
||||
if (!isValidPath(oldPath) || !isValidPath(newPath)) {
|
||||
qWarning() << "Cannot perform rename with invalid paths - both should have leading forward slashes:"
|
||||
<< oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// figure out if this rename is for a file or folder
|
||||
if (pathIsFolder(oldPath)) {
|
||||
if (!pathIsFolder(newPath)) {
|
||||
// we were asked to rename a path to a folder to a path that isn't a folder, this is a fail
|
||||
qWarning() << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// take a copy of the old mappings
|
||||
auto oldMappings = _fileMappings;
|
||||
|
||||
// iterate the current mappings and adjust any that matches the renamed folder
|
||||
auto it = oldMappings.begin();
|
||||
|
||||
while (it != oldMappings.end()) {
|
||||
if (it.key().startsWith(oldPath)) {
|
||||
auto newKey = it.key();
|
||||
newKey.replace(0, oldPath.size(), newPath);
|
||||
|
||||
// remove the old version from the in memory file mappings
|
||||
_fileMappings.remove(it.key());
|
||||
_fileMappings.insert(newKey, it.value());
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
if (writeMappingsToFile()) {
|
||||
// persisted the changed mappings, return success
|
||||
qDebug() << "Renamed folder mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// couldn't persist the renamed paths, rollback and return failure
|
||||
_fileMappings = oldMappings;
|
||||
|
||||
qWarning() << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (pathIsFolder(newPath)) {
|
||||
// we were asked to rename a path to a file to a path that is a folder, this is a fail
|
||||
qWarning() << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// take the old hash to remove the old mapping
|
||||
auto oldSourceMapping = _fileMappings.take(oldPath).toString();
|
||||
|
||||
// in case we're overwriting, keep the current destination mapping for potential rollback
|
||||
auto oldDestinationMapping = _fileMappings.value(newPath);
|
||||
|
||||
if (!oldSourceMapping.isEmpty()) {
|
||||
_fileMappings[newPath] = oldSourceMapping;
|
||||
|
||||
if (writeMappingsToFile()) {
|
||||
// persisted the renamed mapping, return success
|
||||
qDebug() << "Renamed mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// we couldn't persist the renamed mapping, rollback and return failure
|
||||
_fileMappings[oldPath] = oldSourceMapping;
|
||||
|
||||
if (!oldDestinationMapping.isNull()) {
|
||||
// put back the overwritten mapping for the destination path
|
||||
_fileMappings[newPath] = oldDestinationMapping.toString();
|
||||
} else {
|
||||
// clear the new mapping
|
||||
_fileMappings.remove(newPath);
|
||||
}
|
||||
|
||||
qDebug() << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// failed to find a mapping that was to be renamed, return failure
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
#ifndef hifi_AssetServer_h
|
||||
#define hifi_AssetServer_h
|
||||
|
||||
#include <QDir>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QThreadPool>
|
||||
|
||||
#include <ThreadedAssignment.h>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ReceivedMessage.h"
|
||||
|
@ -34,17 +34,39 @@ private slots:
|
|||
void handleAssetGetInfo(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
|
||||
void handleAssetGet(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
|
||||
void handleAssetUpload(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer senderNode);
|
||||
void handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
||||
void sendStatsPacket();
|
||||
|
||||
private:
|
||||
static void writeError(NLPacketList* packetList, AssetServerError error);
|
||||
using Mappings = QVariantHash;
|
||||
|
||||
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
|
||||
// Mapping file operations must be called from main assignment thread only
|
||||
void loadMappingsFromFile();
|
||||
bool writeMappingsToFile();
|
||||
|
||||
/// Set the mapping for path to hash
|
||||
bool setMapping(AssetPath path, AssetHash hash);
|
||||
|
||||
/// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`.
|
||||
bool deleteMappings(AssetPathList& paths);
|
||||
|
||||
/// Rename mapping from `oldPath` to `newPath`. Returns true if successful
|
||||
bool renameMapping(AssetPath oldPath, AssetPath newPath);
|
||||
|
||||
void performMappingMigration();
|
||||
|
||||
Mappings _fileMappings;
|
||||
|
||||
QDir _resourcesDirectory;
|
||||
QDir _filesDirectory;
|
||||
QThreadPool _taskPool;
|
||||
};
|
||||
|
||||
inline void writeError(NLPacketList* packetList, AssetServerError error) {
|
||||
packetList->writePrimitive(error);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -33,13 +33,10 @@ SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const Shar
|
|||
|
||||
void SendAssetTask::run() {
|
||||
MessageID messageID;
|
||||
uint8_t extensionLength;
|
||||
DataOffset start, end;
|
||||
|
||||
_message->readPrimitive(&messageID);
|
||||
QByteArray assetHash = _message->read(SHA256_HASH_LENGTH);
|
||||
_message->readPrimitive(&extensionLength);
|
||||
QByteArray extension = _message->read(extensionLength);
|
||||
|
||||
// `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`.
|
||||
// `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data,
|
||||
|
@ -59,15 +56,15 @@ void SendAssetTask::run() {
|
|||
replyPacketList->writePrimitive(messageID);
|
||||
|
||||
if (end <= start) {
|
||||
writeError(replyPacketList.get(), AssetServerError::InvalidByteRange);
|
||||
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
|
||||
} else {
|
||||
QString filePath = _resourcesDir.filePath(QString(hexHash) + "." + QString(extension));
|
||||
QString filePath = _resourcesDir.filePath(QString(hexHash));
|
||||
|
||||
QFile file { filePath };
|
||||
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
if (file.size() < end) {
|
||||
writeError(replyPacketList.get(), AssetServerError::InvalidByteRange);
|
||||
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
|
||||
qCDebug(networking) << "Bad byte range: " << hexHash << " " << start << ":" << end;
|
||||
} else {
|
||||
auto size = end - start;
|
||||
|
@ -80,7 +77,7 @@ void SendAssetTask::run() {
|
|||
file.close();
|
||||
} else {
|
||||
qCDebug(networking) << "Asset not found: " << filePath << "(" << hexHash << ")";
|
||||
writeError(replyPacketList.get(), AssetServerError::AssetNotFound);
|
||||
replyPacketList->writePrimitive(AssetServerError::AssetNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,15 +37,10 @@ void UploadAssetTask::run() {
|
|||
MessageID messageID;
|
||||
buffer.read(reinterpret_cast<char*>(&messageID), sizeof(messageID));
|
||||
|
||||
uint8_t extensionLength;
|
||||
buffer.read(reinterpret_cast<char*>(&extensionLength), sizeof(extensionLength));
|
||||
|
||||
QByteArray extension = buffer.read(extensionLength);
|
||||
|
||||
uint64_t fileSize;
|
||||
buffer.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
|
||||
|
||||
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes and extension" << extension << "from"
|
||||
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from"
|
||||
<< uuidStringWithoutCurlyBraces(_senderNode->getUUID());
|
||||
|
||||
auto replyPacket = NLPacket::create(PacketType::AssetUploadReply);
|
||||
|
@ -62,17 +57,47 @@ void UploadAssetTask::run() {
|
|||
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID())
|
||||
<< "is: (" << hexHash << ") ";
|
||||
|
||||
QFile file { _resourcesDir.filePath(QString(hexHash)) + "." + QString(extension) };
|
||||
QFile file { _resourcesDir.filePath(QString(hexHash)) };
|
||||
|
||||
bool existingCorrectFile = false;
|
||||
|
||||
if (file.exists()) {
|
||||
qDebug() << "[WARNING] This file already exists: " << hexHash;
|
||||
} else {
|
||||
file.open(QIODevice::WriteOnly);
|
||||
file.write(fileData);
|
||||
file.close();
|
||||
// check if the local file has the correct contents, otherwise we overwrite
|
||||
if (file.open(QIODevice::ReadOnly) && hashData(file.readAll()) == hash) {
|
||||
qDebug() << "Not overwriting existing verified file: " << hexHash;
|
||||
|
||||
existingCorrectFile = true;
|
||||
|
||||
replyPacket->writePrimitive(AssetServerError::NoError);
|
||||
replyPacket->write(hash);
|
||||
} else {
|
||||
qDebug() << "Overwriting an existing file whose contents did not match the expected hash: " << hexHash;
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
replyPacket->writePrimitive(AssetServerError::NoError);
|
||||
replyPacket->write(hash);
|
||||
|
||||
if (!existingCorrectFile) {
|
||||
if (file.open(QIODevice::WriteOnly) && file.write(fileData) == qint64(fileSize)) {
|
||||
qDebug() << "Wrote file" << hexHash << "to disk. Upload complete";
|
||||
file.close();
|
||||
|
||||
replyPacket->writePrimitive(AssetServerError::NoError);
|
||||
replyPacket->write(hash);
|
||||
} else {
|
||||
qWarning() << "Failed to upload or write to file" << hexHash << " - upload failed.";
|
||||
|
||||
// upload has failed - remove the file and return an error
|
||||
auto removed = file.remove();
|
||||
|
||||
if (!removed) {
|
||||
qWarning() << "Removal of failed upload file" << hexHash << "failed.";
|
||||
}
|
||||
|
||||
replyPacket->writePrimitive(AssetServerError::FileOperationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
|
|
@ -369,14 +369,6 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) {
|
|||
reverbTime = _zoneReverbSettings[i].reverbTime;
|
||||
wetLevel = _zoneReverbSettings[i].wetLevel;
|
||||
|
||||
// Modulate wet level with distance to wall
|
||||
float MIN_ATTENUATION_DISTANCE = 2.0f;
|
||||
float MAX_ATTENUATION = -12; // dB
|
||||
glm::vec3 distanceToWalls = (box.getDimensions() / 2.0f) - glm::abs(streamPosition - box.calcCenter());
|
||||
float distanceToClosestWall = glm::min(distanceToWalls.x, distanceToWalls.z);
|
||||
if (distanceToClosestWall < MIN_ATTENUATION_DISTANCE) {
|
||||
wetLevel += MAX_ATTENUATION * (1.0f - distanceToClosestWall / MIN_ATTENUATION_DISTANCE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -835,41 +827,40 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
|
|||
if (audioEnvGroupObject[AUDIO_ZONES].isObject()) {
|
||||
const QJsonObject& zones = audioEnvGroupObject[AUDIO_ZONES].toObject();
|
||||
|
||||
const QString X_RANGE = "x_range";
|
||||
const QString Y_RANGE = "y_range";
|
||||
const QString Z_RANGE = "z_range";
|
||||
const QString X_MIN = "x_min";
|
||||
const QString X_MAX = "x_max";
|
||||
const QString Y_MIN = "y_min";
|
||||
const QString Y_MAX = "y_max";
|
||||
const QString Z_MIN = "z_min";
|
||||
const QString Z_MAX = "z_max";
|
||||
foreach (const QString& zone, zones.keys()) {
|
||||
QJsonObject zoneObject = zones[zone].toObject();
|
||||
|
||||
if (zoneObject.contains(X_RANGE) && zoneObject.contains(Y_RANGE) && zoneObject.contains(Z_RANGE)) {
|
||||
QStringList xRange = zoneObject.value(X_RANGE).toString().split("-", QString::SkipEmptyParts);
|
||||
QStringList yRange = zoneObject.value(Y_RANGE).toString().split("-", QString::SkipEmptyParts);
|
||||
QStringList zRange = zoneObject.value(Z_RANGE).toString().split("-", QString::SkipEmptyParts);
|
||||
if (zoneObject.contains(X_MIN) && zoneObject.contains(X_MAX) && zoneObject.contains(Y_MIN) &&
|
||||
zoneObject.contains(Y_MAX) && zoneObject.contains(Z_MIN) && zoneObject.contains(Z_MAX)) {
|
||||
|
||||
if (xRange.size() == 2 && yRange.size() == 2 && zRange.size() == 2) {
|
||||
float xMin, xMax, yMin, yMax, zMin, zMax;
|
||||
bool ok, allOk = true;
|
||||
xMin = xRange[0].toFloat(&ok);
|
||||
allOk &= ok;
|
||||
xMax = xRange[1].toFloat(&ok);
|
||||
allOk &= ok;
|
||||
yMin = yRange[0].toFloat(&ok);
|
||||
allOk &= ok;
|
||||
yMax = yRange[1].toFloat(&ok);
|
||||
allOk &= ok;
|
||||
zMin = zRange[0].toFloat(&ok);
|
||||
allOk &= ok;
|
||||
zMax = zRange[1].toFloat(&ok);
|
||||
allOk &= ok;
|
||||
float xMin, xMax, yMin, yMax, zMin, zMax;
|
||||
bool ok, allOk = true;
|
||||
xMin = zoneObject.value(X_MIN).toString().toFloat(&ok);
|
||||
allOk &= ok;
|
||||
xMax = zoneObject.value(X_MAX).toString().toFloat(&ok);
|
||||
allOk &= ok;
|
||||
yMin = zoneObject.value(Y_MIN).toString().toFloat(&ok);
|
||||
allOk &= ok;
|
||||
yMax = zoneObject.value(Y_MAX).toString().toFloat(&ok);
|
||||
allOk &= ok;
|
||||
zMin = zoneObject.value(Z_MIN).toString().toFloat(&ok);
|
||||
allOk &= ok;
|
||||
zMax = zoneObject.value(Z_MAX).toString().toFloat(&ok);
|
||||
allOk &= ok;
|
||||
|
||||
if (allOk) {
|
||||
glm::vec3 corner(xMin, yMin, zMin);
|
||||
glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin);
|
||||
AABox zoneAABox(corner, dimensions);
|
||||
_audioZones.insert(zone, zoneAABox);
|
||||
qDebug() << "Added zone:" << zone << "(corner:" << corner
|
||||
<< ", dimensions:" << dimensions << ")";
|
||||
}
|
||||
if (allOk) {
|
||||
glm::vec3 corner(xMin, yMin, zMin);
|
||||
glm::vec3 dimensions(xMax - xMin, yMax - yMin, zMax - zMin);
|
||||
AABox zoneAABox(corner, dimensions);
|
||||
_audioZones.insert(zone, zoneAABox);
|
||||
qDebug() << "Added zone:" << zone << "(corner:" << corner
|
||||
<< ", dimensions:" << dimensions << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
#include "AssignmentParentFinder.h"
|
||||
|
||||
SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool& success) const {
|
||||
SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool& success, SpatialParentTree* entityTree) const {
|
||||
SpatiallyNestableWeakPointer parent;
|
||||
|
||||
if (parentID.isNull()) {
|
||||
|
@ -20,7 +20,11 @@ SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool&
|
|||
}
|
||||
|
||||
// search entities
|
||||
parent = _tree->findEntityByEntityItemID(parentID);
|
||||
if (entityTree) {
|
||||
parent = entityTree->findByID(parentID);
|
||||
} else {
|
||||
parent = _tree->findEntityByEntityItemID(parentID);
|
||||
}
|
||||
if (parent.expired()) {
|
||||
success = false;
|
||||
} else {
|
||||
|
|
|
@ -25,7 +25,7 @@ class AssignmentParentFinder : public SpatialParentFinder {
|
|||
public:
|
||||
AssignmentParentFinder(EntityTreePointer tree) : _tree(tree) { }
|
||||
virtual ~AssignmentParentFinder() { }
|
||||
virtual SpatiallyNestableWeakPointer find(QUuid parentID, bool& success) const;
|
||||
virtual SpatiallyNestableWeakPointer find(QUuid parentID, bool& success, SpatialParentTree* entityTree = nullptr) const;
|
||||
|
||||
protected:
|
||||
EntityTreePointer _tree;
|
||||
|
|
|
@ -272,19 +272,34 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
|||
tree->setWantTerseEditLogging(wantTerseEditLogging);
|
||||
}
|
||||
|
||||
void EntityServer::nodeAdded(SharedNodePointer node) {
|
||||
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
|
||||
tree->knowAvatarID(node->getUUID());
|
||||
OctreeServer::nodeAdded(node);
|
||||
}
|
||||
|
||||
void EntityServer::nodeKilled(SharedNodePointer node) {
|
||||
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
|
||||
tree->deleteDescendantsOfAvatar(node->getUUID());
|
||||
tree->forgetAvatarID(node->getUUID());
|
||||
OctreeServer::nodeKilled(node);
|
||||
}
|
||||
|
||||
// FIXME - this stats tracking is somewhat temporary to debug the Whiteboard issues. It's not a bad
|
||||
// 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() {
|
||||
|
|
|
@ -27,6 +27,8 @@ struct ViewerSendingStats {
|
|||
quint64 lastEdited;
|
||||
};
|
||||
|
||||
class SimpleEntitySimulation;
|
||||
|
||||
class EntityServer : public OctreeServer, public NewlyCreatedEntityHook {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -52,10 +54,12 @@ 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:
|
||||
virtual void nodeAdded(SharedNodePointer node);
|
||||
virtual void nodeKilled(SharedNodePointer node);
|
||||
void pruneDeletedEntities();
|
||||
|
||||
protected:
|
||||
|
@ -65,7 +69,7 @@ private slots:
|
|||
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
||||
private:
|
||||
EntitySimulation* _entitySimulation;
|
||||
SimpleEntitySimulation* _entitySimulation;
|
||||
QTimer* _pruneDeletedEntitiesTimer = nullptr;
|
||||
|
||||
QReadWriteLock _viewerSendingStatsLock;
|
||||
|
|
|
@ -153,7 +153,7 @@ bool OctreeQueryNode::updateCurrentViewFrustum() {
|
|||
newestViewFrustum.setPosition(getCameraPosition());
|
||||
newestViewFrustum.setOrientation(getCameraOrientation());
|
||||
|
||||
newestViewFrustum.setKeyholeRadius(getKeyholeRadius());
|
||||
newestViewFrustum.setCenterRadius(getCameraCenterRadius());
|
||||
|
||||
// Also make sure it's got the correct lens details from the camera
|
||||
float originalFOV = getCameraFov();
|
||||
|
|
|
@ -124,7 +124,7 @@ AtomicUIntStat OctreeSendThread::_totalSpecialPackets { 0 };
|
|||
|
||||
|
||||
int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent,
|
||||
int& truePacketsSent) {
|
||||
int& truePacketsSent, bool dontSuppressDuplicate) {
|
||||
OctreeServer::didHandlePacketSend(this);
|
||||
|
||||
// if we're shutting down, then exit early
|
||||
|
@ -141,7 +141,7 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
|
|||
// Here's where we check to see if this packet is a duplicate of the last packet. If it is, we will silently
|
||||
// obscure the packet and not send it. This allows the callers and upper level logic to not need to know about
|
||||
// this rate control savings.
|
||||
if (nodeData->shouldSuppressDuplicatePacket()) {
|
||||
if (!dontSuppressDuplicate && nodeData->shouldSuppressDuplicatePacket()) {
|
||||
nodeData->resetOctreePacket(); // we still need to reset it though!
|
||||
return packetsSent; // without sending...
|
||||
}
|
||||
|
@ -356,7 +356,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
|
|||
//unsigned long encodeTime = nodeData->stats.getTotalEncodeTime();
|
||||
//unsigned long elapsedTime = nodeData->stats.getElapsedTime();
|
||||
|
||||
int packetsJustSent = handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent);
|
||||
int packetsJustSent = handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent, isFullScene);
|
||||
packetsSentThisInterval += packetsJustSent;
|
||||
|
||||
// If we're starting a full scene, then definitely we want to empty the elementBag
|
||||
|
@ -582,6 +582,18 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
|
|||
if (nodeData->elementBag.isEmpty()) {
|
||||
nodeData->updateLastKnownViewFrustum();
|
||||
nodeData->setViewSent(true);
|
||||
|
||||
// If this was a full scene then make sure we really send out a stats packet at this point so that
|
||||
// the clients will know the scene is stable
|
||||
if (isFullScene) {
|
||||
int thisTrueBytesSent = 0;
|
||||
int thisTruePacketsSent = 0;
|
||||
nodeData->stats.sceneCompleted();
|
||||
int packetsJustSent = handlePacketSend(node, nodeData, thisTrueBytesSent, thisTruePacketsSent, true);
|
||||
_totalBytes += thisTrueBytesSent;
|
||||
_totalPackets += thisTruePacketsSent;
|
||||
truePacketsSent += packetsJustSent;
|
||||
}
|
||||
}
|
||||
|
||||
} // end if bag wasn't empty, and so we sent stuff...
|
||||
|
|
|
@ -50,7 +50,7 @@ protected:
|
|||
virtual bool process();
|
||||
|
||||
private:
|
||||
int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, int& truePacketsSent);
|
||||
int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, int& truePacketsSent, bool dontSuppressDuplicate = false);
|
||||
int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged);
|
||||
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
@ -985,7 +981,7 @@ void OctreeServer::readConfiguration() {
|
|||
_settings = settingsSectionObject; // keep this for later
|
||||
|
||||
if (!readOptionString(QString("statusHost"), settingsSectionObject, _statusHost) || _statusHost.isEmpty()) {
|
||||
_statusHost = getLocalAddress().toString();
|
||||
_statusHost = getGuessedLocalAddress().toString();
|
||||
}
|
||||
qDebug("statusHost=%s", qPrintable(_statusHost));
|
||||
|
||||
|
|
|
@ -127,8 +127,8 @@ public:
|
|||
public slots:
|
||||
/// runs the octree server assignment
|
||||
void run();
|
||||
void nodeAdded(SharedNodePointer node);
|
||||
void nodeKilled(SharedNodePointer node);
|
||||
virtual void nodeAdded(SharedNodePointer node);
|
||||
virtual void nodeKilled(SharedNodePointer node);
|
||||
void sendStatsPacket();
|
||||
|
||||
private slots:
|
||||
|
|
23
cmake/externals/LibOVR/CMakeLists.txt
vendored
23
cmake/externals/LibOVR/CMakeLists.txt
vendored
|
@ -12,19 +12,16 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
|||
# 0.5 public
|
||||
# URL http://static.oculus.com/sdk-downloads/ovr_sdk_win_0.5.0.1.zip
|
||||
# URL_MD5 d3fc4c02db9be5ff08af4ef4c97b32f9
|
||||
# 0.6 public
|
||||
# URL http://static.oculus.com/sdk-downloads/0.6.0.1/Public/1435190862/ovr_sdk_win_0.6.0.1.zip
|
||||
# URL_MD5 4b3ef825f9a1d6d3035c9f6820687da9
|
||||
# 0.8 public
|
||||
# URL http://static.oculus.com/sdk-downloads/0.8.0.0/Public/1445451746/ovr_sdk_win_0.8.0.0.zip
|
||||
# URL_MD5 54944b03b95149d6010f84eb701b9647
|
||||
# 1.3 public
|
||||
# URL http://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.3.0_public.zip
|
||||
# URL_MD5 4d26faba0c1f35ff80bf674c96ed9259
|
||||
|
||||
if (WIN32)
|
||||
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://static.oculus.com/sdk-downloads/0.8.0.0/Public/1445451746/ovr_sdk_win_0.8.0.0.zip
|
||||
URL_MD5 54944b03b95149d6010f84eb701b9647
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.3.0_public.zip
|
||||
URL_MD5 a2dcf695e0f03a70fdd1ed7480585e82
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
|
@ -33,14 +30,16 @@ if (WIN32)
|
|||
|
||||
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
|
||||
|
||||
# FIXME need to account for different architectures
|
||||
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/LibOVR/Include CACHE TYPE INTERNAL)
|
||||
set(LIBOVR_DIR ${SOURCE_DIR}/OculusSDK/LibOVR)
|
||||
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/LibOVR/Lib/Windows/x64/Release/VS2013/LibOVR.lib CACHE TYPE INTERNAL)
|
||||
set(LIBOVR_LIB_DIR ${LIBOVR_DIR}/Lib/Windows/x64/Release/VS2013 CACHE TYPE INTERNAL)
|
||||
else()
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${SOURCE_DIR}/LibOVR/Lib/Windows/Win32/Release/VS2013/LibOVR.lib CACHE TYPE INTERNAL)
|
||||
set(LIBOVR_LIB_DIR ${LIBOVR_DIR}/Lib/Windows/Win32/Release/VS2013 CACHE TYPE INTERNAL)
|
||||
endif()
|
||||
|
||||
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${LIBOVR_DIR}/Include CACHE TYPE INTERNAL)
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${LIBOVR_LIB_DIR}/LibOVR.lib CACHE TYPE INTERNAL)
|
||||
|
||||
elseif(APPLE)
|
||||
|
||||
ExternalProject_Add(
|
||||
|
|
2
cmake/externals/oglplus/CMakeLists.txt
vendored
2
cmake/externals/oglplus/CMakeLists.txt
vendored
|
@ -4,7 +4,7 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
|||
include(ExternalProject)
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://iweb.dl.sourceforge.net/project/oglplus/oglplus-0.63.x/oglplus-0.63.0.zip
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/oglplus-0.63.0.zip
|
||||
URL_MD5 de984ab245b185b45c87415c0e052135
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
|
|
4
cmake/externals/openvr/CMakeLists.txt
vendored
4
cmake/externals/openvr/CMakeLists.txt
vendored
|
@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
|||
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL https://github.com/ValveSoftware/openvr/archive/v0.9.15.zip
|
||||
URL_MD5 0ff8560b49b6da1150fcc47360e8ceca
|
||||
URL https://github.com/ValveSoftware/openvr/archive/v0.9.19.zip
|
||||
URL_MD5 843f9dde488584d8af1f3ecf2252b4e0
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
|
|
|
@ -46,6 +46,8 @@ function(AUTOSCRIBE_SHADER SHADER_FILE)
|
|||
set(SHADER_TARGET ${SHADER_TARGET}_frag.h)
|
||||
endif()
|
||||
|
||||
set(SHADER_TARGET "${SHADERS_DIR}/${SHADER_TARGET}")
|
||||
|
||||
# Target dependant Custom rule on the SHADER_FILE
|
||||
if (APPLE)
|
||||
set(GLPROFILE MAC_GL)
|
||||
|
@ -87,10 +89,14 @@ macro(AUTOSCRIBE_SHADER_LIB)
|
|||
file(GLOB_RECURSE SHADER_INCLUDE_FILES src/*.slh)
|
||||
file(GLOB_RECURSE SHADER_SOURCE_FILES src/*.slv src/*.slf)
|
||||
|
||||
#make the shader folder
|
||||
set(SHADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders/${TARGET_NAME}")
|
||||
file(MAKE_DIRECTORY ${SHADERS_DIR})
|
||||
|
||||
#message(${SHADER_INCLUDE_FILES})
|
||||
foreach(SHADER_FILE ${SHADER_SOURCE_FILES})
|
||||
AUTOSCRIBE_SHADER(${SHADER_FILE} ${SHADER_INCLUDE_FILES})
|
||||
file(TO_CMAKE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${AUTOSCRIBE_SHADER_RETURN}" AUTOSCRIBE_GENERATED_FILE)
|
||||
file(TO_CMAKE_PATH "${AUTOSCRIBE_SHADER_RETURN}" AUTOSCRIBE_GENERATED_FILE)
|
||||
list(APPEND AUTOSCRIBE_SHADER_SRC ${AUTOSCRIBE_GENERATED_FILE})
|
||||
endforeach()
|
||||
#message(${AUTOSCRIBE_SHADER_SRC})
|
||||
|
@ -105,4 +111,7 @@ macro(AUTOSCRIBE_SHADER_LIB)
|
|||
list(APPEND AUTOSCRIBE_SHADER_LIB_SRC ${SHADER_SOURCE_FILES})
|
||||
list(APPEND AUTOSCRIBE_SHADER_LIB_SRC ${AUTOSCRIBE_SHADER_SRC})
|
||||
|
||||
# Link library shaders, if they exist
|
||||
include_directories("${SHADERS_DIR}")
|
||||
|
||||
endmacro()
|
||||
|
|
|
@ -85,8 +85,8 @@ macro(GENERATE_INSTALLERS)
|
|||
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
|
||||
|
||||
cpack_add_component(${CLIENT_COMPONENT} DISPLAY_NAME "High Fidelity Client")
|
||||
cpack_add_component(${SERVER_COMPONENT} DISPLAY_NAME "High Fidelity Server")
|
||||
cpack_add_component(${CLIENT_COMPONENT} DISPLAY_NAME "High Fidelity Interface")
|
||||
cpack_add_component(${SERVER_COMPONENT} DISPLAY_NAME "High Fidelity Sandbox")
|
||||
|
||||
include(CPack)
|
||||
endmacro()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -19,20 +19,14 @@ macro(LINK_HIFI_LIBRARIES)
|
|||
endif ()
|
||||
|
||||
include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src")
|
||||
include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}/shaders")
|
||||
|
||||
add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY})
|
||||
|
||||
# link the actual library - it is static so don't bubble it up
|
||||
target_link_libraries(${TARGET_NAME} ${HIFI_LIBRARY})
|
||||
|
||||
# ask the library what its include dependencies are and link them
|
||||
get_target_property(LINKED_TARGET_DEPENDENCY_INCLUDES ${HIFI_LIBRARY} DEPENDENCY_INCLUDES)
|
||||
|
||||
if(LINKED_TARGET_DEPENDENCY_INCLUDES)
|
||||
list(APPEND ${TARGET_NAME}_DEPENDENCY_INCLUDES ${LINKED_TARGET_DEPENDENCY_INCLUDES})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
setup_memory_debugger()
|
||||
|
||||
endmacro(LINK_HIFI_LIBRARIES)
|
||||
endmacro(LINK_HIFI_LIBRARIES)
|
||||
|
|
34
cmake/macros/ManuallyInstallOpenSSLForQt.cmake
Normal file
34
cmake/macros/ManuallyInstallOpenSSLForQt.cmake
Normal 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()
|
|
@ -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()
|
|
@ -53,7 +53,7 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME})
|
||||
|
||||
set(CONSOLE_EXEC_NAME "Server Console.app")
|
||||
set(CONSOLE_EXEC_NAME "Sandbox.app")
|
||||
set(CONSOLE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${CONSOLE_EXEC_NAME}")
|
||||
|
||||
set(CONSOLE_APP_CONTENTS "${CONSOLE_INSTALL_APP_PATH}/Contents")
|
||||
|
@ -84,12 +84,19 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
|
||||
# shortcut names
|
||||
if (PRODUCTION_BUILD)
|
||||
set(INTERFACE_SHORTCUT_NAME "High Fidelity")
|
||||
set(CONSOLE_SHORTCUT_NAME "Server Console")
|
||||
set(INTERFACE_SHORTCUT_NAME "Interface")
|
||||
set(CONSOLE_SHORTCUT_NAME "Sandbox")
|
||||
else ()
|
||||
set(INTERFACE_SHORTCUT_NAME "High Fidelity - ${BUILD_VERSION}")
|
||||
set(CONSOLE_SHORTCUT_NAME "Server Console - ${BUILD_VERSION}")
|
||||
set(INTERFACE_SHORTCUT_NAME "Interface - ${BUILD_VERSION}")
|
||||
set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION}")
|
||||
endif ()
|
||||
|
||||
set(INTERFACE_HF_SHORTCUT_NAME "High Fidelity ${INTERFACE_SHORTCUT_NAME}")
|
||||
set(CONSOLE_HF_SHORTCUT_NAME "High Fidelity ${CONSOLE_SHORTCUT_NAME}")
|
||||
|
||||
set(PRE_SANDBOX_INTERFACE_SHORTCUT_NAME "High Fidelity")
|
||||
set(PRE_SANDBOX_CONSOLE_SHORTCUT_NAME "Server Console")
|
||||
|
||||
# check if we need to find signtool
|
||||
if (PRODUCTION_BUILD OR PR_BUILD)
|
||||
find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/8.1" PATH_SUFFIXES "bin/x64")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -10,10 +10,14 @@
|
|||
#
|
||||
|
||||
set(INTERFACE_SHORTCUT_NAME "@INTERFACE_SHORTCUT_NAME@")
|
||||
set(INTERFACE_HF_SHORTCUT_NAME "@INTERFACE_HF_SHORTCUT_NAME@")
|
||||
set(INTERFACE_WIN_EXEC_NAME "@INTERFACE_EXEC_PREFIX@.exe")
|
||||
set(CONSOLE_INSTALL_SUBDIR "@CONSOLE_INSTALL_DIR@")
|
||||
set(CONSOLE_SHORTCUT_NAME "@CONSOLE_SHORTCUT_NAME@")
|
||||
set(CONSOLE_HF_SHORTCUT_NAME "@CONSOLE_HF_SHORTCUT_NAME@")
|
||||
set(CONSOLE_WIN_EXEC_NAME "@CONSOLE_EXEC_NAME@")
|
||||
set(PRE_SANDBOX_INTERFACE_SHORTCUT_NAME "@PRE_SANDBOX_INTERFACE_SHORTCUT_NAME@")
|
||||
set(PRE_SANDBOX_CONSOLE_SHORTCUT_NAME "@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@")
|
||||
set(DS_EXEC_NAME "@DS_EXEC_NAME@")
|
||||
set(AC_EXEC_NAME "@AC_EXEC_NAME@")
|
||||
set(HIGH_FIDELITY_PROTOCOL "@HIGH_FIDELITY_PROTOCOL@")
|
||||
|
|
|
@ -271,8 +271,9 @@ FunctionEnd
|
|||
|
||||
@CPACK_NSIS_PAGE_COMPONENTS@
|
||||
|
||||
Page custom PostInstallOptionsPage ReadPostInstallOptions
|
||||
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
Page custom PostInstallOptionsPage HandlePostInstallOptions
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
@ -341,6 +342,227 @@ FunctionEnd
|
|||
;Only for solid compression (by default, solid compression is enabled for BZIP2 and LZMA)
|
||||
|
||||
ReserveFile "@POST_INSTALL_OPTIONS_PATH@"
|
||||
; Make sure nsDialogs is included before we use it
|
||||
!include "nsdialogs.nsh"
|
||||
|
||||
;--------------------------------
|
||||
; Post Install Options
|
||||
|
||||
Var PostInstallDialog
|
||||
Var DesktopClientCheckbox
|
||||
Var DesktopServerCheckbox
|
||||
Var ServerStartupCheckbox
|
||||
Var LaunchNowCheckbox
|
||||
Var CurrentOffset
|
||||
Var OffsetUnits
|
||||
Var CopyFromProductionCheckbox
|
||||
|
||||
!macro SetPostInstallOption Checkbox OptionName Default
|
||||
; reads the value for the given post install option to the registry
|
||||
ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}"
|
||||
|
||||
${If} $0 == "NO"
|
||||
; the value in the registry says it should not be checked
|
||||
${NSD_SetState} ${Checkbox} ${BST_UNCHECKED}
|
||||
${ElseIf} $0 == "YES"
|
||||
; the value in the registry says it should be checked
|
||||
${NSD_SetState} ${Checkbox} ${BST_CHECKED}
|
||||
${Else}
|
||||
; the value in the registry was not in the expected format, use default
|
||||
${NSD_SetState} ${Checkbox} ${Default}
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
Function PostInstallOptionsPage
|
||||
!insertmacro MUI_HEADER_TEXT "Setup Options" ""
|
||||
|
||||
nsDialogs::Create 1018
|
||||
Pop $PostInstallDialog
|
||||
|
||||
${If} $PostInstallDialog == error
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
StrCpy $CurrentOffset 0
|
||||
StrCpy $OffsetUnits u
|
||||
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_HF_SHORTCUT_NAME@"
|
||||
Pop $DesktopClientCheckbox
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED}
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @CONSOLE_HF_SHORTCUT_NAME@"
|
||||
Pop $DesktopServerCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED}
|
||||
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup"
|
||||
Pop $ServerStartupCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
|
||||
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ after install"
|
||||
${Else}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
|
||||
${EndIf}
|
||||
|
||||
Pop $LaunchNowCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
|
||||
|
||||
${If} @PR_BUILD@ == 1
|
||||
; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED}
|
||||
${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED}
|
||||
${EndIf}
|
||||
|
||||
; push the offset
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install"
|
||||
Pop $CopyFromProductionCheckbox
|
||||
|
||||
${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED}
|
||||
${EndIf}
|
||||
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
!macro WritePostInstallOption OptionName Option
|
||||
; writes the value for the given post install option to the registry
|
||||
WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option}
|
||||
!macroend
|
||||
|
||||
Var DesktopClientState
|
||||
Var DesktopServerState
|
||||
Var ServerStartupState
|
||||
Var LaunchNowState
|
||||
Var CopyFromProductionState
|
||||
|
||||
Function ReadPostInstallOptions
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
; check if the user asked for a desktop shortcut to High Fidelity
|
||||
${NSD_GetState} $DesktopClientCheckbox $DesktopClientState
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
; check if the user asked for a desktop shortcut to Sandbox
|
||||
${NSD_GetState} $DesktopServerCheckbox $DesktopServerState
|
||||
|
||||
; check if the user asked to have Sandbox launched every startup
|
||||
${NSD_GetState} $ServerStartupCheckbox $ServerStartupState
|
||||
${EndIf}
|
||||
|
||||
${If} @PR_BUILD@ == 1
|
||||
; check if we need to copy settings/content from production for this PR build
|
||||
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
|
||||
${EndIf}
|
||||
|
||||
; check if we need to launch an application post-install
|
||||
${NSD_GetState} $LaunchNowCheckbox $LaunchNowState
|
||||
FunctionEnd
|
||||
|
||||
Function HandlePostInstallOptions
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
; check if the user asked for a desktop shortcut to High Fidelity
|
||||
${If} $DesktopClientState == ${BST_CHECKED}
|
||||
CreateShortCut "$DESKTOP\@INTERFACE_HF_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"
|
||||
!insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO
|
||||
${EndIf}
|
||||
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
; check if the user asked for a desktop shortcut to Sandbox
|
||||
${If} $DesktopServerState == ${BST_CHECKED}
|
||||
CreateShortCut "$DESKTOP\@CONSOLE_HF_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
|
||||
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO
|
||||
${EndIf}
|
||||
|
||||
; check if the user asked to have Sandbox launched every startup
|
||||
${If} $ServerStartupState == ${BST_CHECKED}
|
||||
; in case we added a shortcut in the global context, pull that now
|
||||
SetShellVarContext all
|
||||
Delete "$SMSTARTUP\@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
|
||||
; make a startup shortcut in this user's current context
|
||||
SetShellVarContext current
|
||||
CreateShortCut "$SMSTARTUP\@CONSOLE_HF_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
|
||||
|
||||
; reset the shell var context back
|
||||
SetShellVarContext all
|
||||
|
||||
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
${If} @PR_BUILD@ == 1
|
||||
|
||||
; check if we need to copy settings/content from production for this PR build
|
||||
${If} $CopyFromProductionState == ${BST_CHECKED}
|
||||
SetShellVarContext current
|
||||
|
||||
StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@"
|
||||
|
||||
; we need to copy whatever is in the data folder for production build to the data folder for this build
|
||||
CreateDirectory $0
|
||||
|
||||
ClearErrors
|
||||
|
||||
; copy the data from production build to this PR build
|
||||
CopyFiles "$APPDATA\High Fidelity\*" $0
|
||||
|
||||
; handle an error in copying files
|
||||
IfErrors 0 NoError
|
||||
|
||||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \
|
||||
"There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually."
|
||||
|
||||
NoError:
|
||||
|
||||
SetShellVarContext all
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
${If} $LaunchNowState == ${BST_CHECKED}
|
||||
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES
|
||||
|
||||
; both launches use the explorer trick in case the user has elevated permissions for the installer
|
||||
; it won't be possible to use this approach if either application should be launched with a command line param
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
|
||||
${Else}
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
|
||||
${EndIf}
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
;--------------------------------
|
||||
;Installer Sections
|
||||
|
@ -367,6 +589,22 @@ Section "-Core installation"
|
|||
Delete "$INSTDIR\version"
|
||||
Delete "$INSTDIR\xinput1_3.dll"
|
||||
|
||||
; Delete old desktop shortcuts before they were renamed during Sandbox rename
|
||||
Delete "$DESKTOP\@PRE_SANDBOX_INTERFACE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$DESKTOP\@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
|
||||
; Delete old Start Menu shortcuts before Sandbox rename
|
||||
Delete "$SMPROGRAMS\$STARTMENU_FOLDER\@PRE_SANDBOX_INTERFACE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$SMPROGRAMS\$STARTMENU_FOLDER\@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
|
||||
; Delete old startup item for Server Console before Sandbox rename
|
||||
SetShellVarContext current
|
||||
Delete "$SMSTARTUP\@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
SetShellVarContext all
|
||||
|
||||
; Rename the incorrectly cased Raleway font
|
||||
Rename "$INSTDIR\resources\qml\styles-uit\RalewaySemibold.qml" "$INSTDIR\resources\qml\styles-uit\RalewaySemiBold.qml"
|
||||
|
||||
; Remove the Old Interface directory and vcredist_x64.exe (from installs prior to Server Console)
|
||||
RMDir /r "$INSTDIR\Interface"
|
||||
Delete "$INSTDIR\vcredist_x64.exe"
|
||||
|
@ -469,233 +707,11 @@ Section "-Core installation"
|
|||
|
||||
@CPACK_NSIS_EXTRA_INSTALL_COMMANDS@
|
||||
|
||||
; Handle whichever post install options were set
|
||||
Call HandlePostInstallOptions
|
||||
|
||||
SectionEnd
|
||||
|
||||
; Make sure nsDialogs is included before we use it
|
||||
!include "nsdialogs.nsh"
|
||||
|
||||
Var PostInstallDialog
|
||||
Var OptionsLabel
|
||||
Var DesktopClientCheckbox
|
||||
Var DesktopServerCheckbox
|
||||
Var ServerStartupCheckbox
|
||||
Var LaunchNowCheckbox
|
||||
Var CurrentOffset
|
||||
Var OffsetUnits
|
||||
Var CopyFromProductionCheckbox
|
||||
|
||||
!macro SetPostInstallOption Checkbox OptionName Default
|
||||
; reads the value for the given post install option to the registry
|
||||
ReadRegStr $0 HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}"
|
||||
|
||||
${If} $0 == "NO"
|
||||
; the value in the registry says it should not be checked
|
||||
${NSD_SetState} ${Checkbox} ${BST_UNCHECKED}
|
||||
${ElseIf} $0 == "YES"
|
||||
; the value in the registry says it should be checked
|
||||
${NSD_SetState} ${Checkbox} ${BST_CHECKED}
|
||||
${Else}
|
||||
; the value in the registry was not in the expected format, use default
|
||||
${NSD_SetState} ${Checkbox} ${Default}
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
Function PostInstallOptionsPage
|
||||
; Set the text on the dialog button to match finish, hide the back and cancel buttons
|
||||
GetDlgItem $R1 $hwndparent 1
|
||||
SendMessage $R1 ${WM_SETTEXT} 0 "STR:&Finish"
|
||||
|
||||
GetDlgItem $R3 $hwndparent 3
|
||||
ShowWindow $R3 0
|
||||
|
||||
nsDialogs::Create 1018
|
||||
Pop $PostInstallDialog
|
||||
|
||||
${If} $PostInstallDialog == error
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 12u "Setup Options"
|
||||
Pop $OptionsLabel
|
||||
|
||||
; Set label to bold
|
||||
CreateFont $R2 "Arial" 10 700
|
||||
SendMessage $OptionsLabel ${WM_SETFONT} $R2 0
|
||||
|
||||
; Force label redraw
|
||||
ShowWindow $OptionsLabel ${SW_HIDE}
|
||||
ShowWindow $OptionsLabel ${SW_SHOW}
|
||||
|
||||
StrCpy $CurrentOffset 15
|
||||
StrCpy $OffsetUnits u
|
||||
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_SHORTCUT_NAME@"
|
||||
Pop $DesktopClientCheckbox
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED}
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for High Fidelity @CONSOLE_SHORTCUT_NAME@"
|
||||
Pop $DesktopServerCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED}
|
||||
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity @CONSOLE_SHORTCUT_NAME@ on startup"
|
||||
Pop $ServerStartupCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
|
||||
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Server Console Now"
|
||||
${Else}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch High Fidelity Now"
|
||||
${EndIf}
|
||||
|
||||
Pop $LaunchNowCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
|
||||
|
||||
${If} @PR_BUILD@ == 1
|
||||
; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED}
|
||||
${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED}
|
||||
${EndIf}
|
||||
|
||||
; push the offset
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install"
|
||||
Pop $CopyFromProductionCheckbox
|
||||
|
||||
${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED}
|
||||
${EndIf}
|
||||
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
!macro WritePostInstallOption OptionName Option
|
||||
; writes the value for the given post install option to the registry
|
||||
WriteRegStr HKLM "@REGISTRY_HKLM_INSTALL_ROOT@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\@POST_INSTALL_OPTIONS_REG_GROUP@" "${OptionName}" ${Option}
|
||||
!macroend
|
||||
|
||||
Var DesktopClientState
|
||||
Var DesktopServerState
|
||||
Var ServerStartupState
|
||||
Var LaunchNowState
|
||||
Var CopyFromProductionState
|
||||
|
||||
Function HandlePostInstallOptions
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
; check if the user asked for a desktop shortcut to High Fidelity
|
||||
${NSD_GetState} $DesktopClientCheckbox $DesktopClientState
|
||||
|
||||
${If} $DesktopClientState == ${BST_CHECKED}
|
||||
CreateShortCut "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"
|
||||
!insertmacro WritePostInstallOption "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@" YES
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ NO
|
||||
${EndIf}
|
||||
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
; check if the user asked for a desktop shortcut to Server Console
|
||||
${NSD_GetState} $DesktopServerCheckbox $DesktopServerState
|
||||
|
||||
${If} $DesktopServerState == ${BST_CHECKED}
|
||||
CreateShortCut "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
|
||||
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ YES
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO
|
||||
${EndIf}
|
||||
|
||||
; check if the user asked to have Server Console launched every startup
|
||||
${NSD_GetState} $ServerStartupCheckbox $ServerStartupState
|
||||
|
||||
${If} $ServerStartupState == ${BST_CHECKED}
|
||||
; in case we added a shortcut in the global context, pull that now
|
||||
SetShellVarContext all
|
||||
Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
|
||||
; make a startup shortcut in this user's current context
|
||||
SetShellVarContext current
|
||||
CreateShortCut "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
|
||||
|
||||
; reset the shell var context back
|
||||
SetShellVarContext all
|
||||
|
||||
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ YES
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
${If} @PR_BUILD@ == 1
|
||||
|
||||
; check if we need to copy settings/content from production for this PR build
|
||||
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
|
||||
|
||||
${If} $CopyFromProductionState == ${BST_CHECKED}
|
||||
SetShellVarContext current
|
||||
|
||||
StrCpy $0 "$APPDATA\@BUILD_ORGANIZATION@"
|
||||
|
||||
; we need to copy whatever is in the data folder for production build to the data folder for this build
|
||||
CreateDirectory $0
|
||||
|
||||
ClearErrors
|
||||
|
||||
; copy the data from production build to this PR build
|
||||
CopyFiles "$APPDATA\High Fidelity\*" $0
|
||||
|
||||
; handle an error in copying files
|
||||
IfErrors 0 NoError
|
||||
|
||||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground \
|
||||
"There was a problem copying your production content and settings to $0 for this PR build.$\r$\n$\r$\nPlease copy them manually."
|
||||
|
||||
NoError:
|
||||
|
||||
SetShellVarContext all
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
; check if we need to launch an application post-install
|
||||
${NSD_GetState} $LaunchNowCheckbox $LaunchNowState
|
||||
|
||||
${If} $LaunchNowState == ${BST_CHECKED}
|
||||
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES
|
||||
|
||||
; both launches use the explorer trick in case the user has elevated permissions for the installer
|
||||
; it won't be possible to use this approach if either application should be launched with a command line param
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
|
||||
${Else}
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
|
||||
${EndIf}
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO
|
||||
${EndIf}
|
||||
|
||||
FunctionEnd
|
||||
|
||||
!include nsProcess.nsh
|
||||
|
||||
!macro PromptForRunningApplication applicationName displayName action prompter
|
||||
|
@ -721,8 +737,8 @@ FunctionEnd
|
|||
!macroend
|
||||
|
||||
!macro CheckForRunningApplications action prompter
|
||||
!insertmacro PromptForRunningApplication "@INTERFACE_WIN_EXEC_NAME@" "High Fidelity client" ${action} ${prompter}
|
||||
!insertmacro PromptForRunningApplication "@CONSOLE_WIN_EXEC_NAME@" "Server Console" ${action} ${prompter}
|
||||
!insertmacro PromptForRunningApplication "@INTERFACE_WIN_EXEC_NAME@" "@CONSOLE_SHORTCUT_NAME@" ${action} ${prompter}
|
||||
!insertmacro PromptForRunningApplication "@CONSOLE_WIN_EXEC_NAME@" "@INTERFACE_SHORTCUT_NAME@" ${action} ${prompter}
|
||||
!insertmacro PromptForRunningApplication "@DS_EXEC_NAME@" "Domain Server" ${action} ${prompter}
|
||||
!insertmacro PromptForRunningApplication "@AC_EXEC_NAME@" "Assignment Client" ${action} ${prompter}
|
||||
!macroend
|
||||
|
@ -866,12 +882,12 @@ Section "Uninstall"
|
|||
Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\@INTERFACE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$SMPROGRAMS\$MUI_TEMP\@CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$DESKTOP\@INTERFACE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$DESKTOP\@CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$DESKTOP\@INTERFACE_HF_SHORTCUT_NAME@.lnk"
|
||||
Delete "$DESKTOP\@CONSOLE_HF_SHORTCUT_NAME@.lnk"
|
||||
|
||||
; if it exists, delete the startup shortcut for the current user
|
||||
SetShellVarContext current
|
||||
Delete "$SMSTARTUP\@CONSOLE_SHORTCUT_NAME@.lnk"
|
||||
Delete "$SMSTARTUP\@CONSOLE_HF_SHORTCUT_NAME@.lnk"
|
||||
SetShellVarContext all
|
||||
|
||||
@CPACK_NSIS_DELETE_ICONS@
|
||||
|
|
|
@ -186,6 +186,15 @@
|
|||
"help": "The path to the directory assets are stored in.<br/>If this path is relative, it will be relative to the application data directory.<br/>If you change this path you will need to manually copy any existing assets from the previous directory.",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "max_bandwidth",
|
||||
"type": "double",
|
||||
"label": "Max Bandwidth Per User",
|
||||
"help": "The maximum upstream bandwidth each user can use (in Mb/s).",
|
||||
"placeholder": "10.0",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -230,22 +239,40 @@
|
|||
},
|
||||
"columns": [
|
||||
{
|
||||
"name": "x_range",
|
||||
"label": "X range",
|
||||
"name": "x_min",
|
||||
"label": "X start",
|
||||
"can_set": true,
|
||||
"placeholder": "0-16384"
|
||||
"placeholder": "-16384.0"
|
||||
},
|
||||
{
|
||||
"name": "y_range",
|
||||
"label": "Y range",
|
||||
"name": "x_max",
|
||||
"label": "X end",
|
||||
"can_set": true,
|
||||
"placeholder": "0-16384"
|
||||
"placeholder": "16384.0"
|
||||
},
|
||||
{
|
||||
"name": "y_min",
|
||||
"label": "Y start",
|
||||
"can_set": true,
|
||||
"placeholder": "-16384.0"
|
||||
},
|
||||
{
|
||||
"name": "z_range",
|
||||
"label": "Z range",
|
||||
"name": "y_max",
|
||||
"label": "Y end",
|
||||
"can_set": true,
|
||||
"placeholder": "0-16384"
|
||||
"placeholder": "16384.0"
|
||||
},
|
||||
{
|
||||
"name": "z_min",
|
||||
"label": "Z start",
|
||||
"can_set": true,
|
||||
"placeholder": "-16384.0"
|
||||
},
|
||||
{
|
||||
"name": "z_max",
|
||||
"label": "Z end",
|
||||
"can_set": true,
|
||||
"placeholder": "16384.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -281,7 +308,7 @@
|
|||
"name": "reverb",
|
||||
"type": "table",
|
||||
"label": "Reverb Settings",
|
||||
"help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet level of -10 db. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet level of -5 db.",
|
||||
"help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.",
|
||||
"numbered": true,
|
||||
"columns": [
|
||||
{
|
||||
|
@ -298,9 +325,9 @@
|
|||
},
|
||||
{
|
||||
"name": "wet_level",
|
||||
"label": "Wet Level",
|
||||
"label": "Wet/Dry Mix",
|
||||
"can_set": true,
|
||||
"placeholder": "(in db)"
|
||||
"placeholder": "(in percent)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -96,15 +96,13 @@ 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
|
||||
|
||||
qDebug() << "Setting up LimitedNodeList and assignments.";
|
||||
setupNodeListAndAssignments();
|
||||
|
||||
loadExistingSessionsFromSettings();
|
||||
|
||||
// setup automatic networking settings with data server
|
||||
setupAutomaticNetworking();
|
||||
|
||||
|
@ -198,7 +196,6 @@ bool DomainServer::optionallySetupOAuth() {
|
|||
}
|
||||
|
||||
AccountManager& accountManager = AccountManager::getInstance();
|
||||
accountManager.disableSettingsFilePersistence();
|
||||
accountManager.setAuthURL(_oauthProviderURL);
|
||||
|
||||
_oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString();
|
||||
|
@ -372,20 +369,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 +390,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 +422,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 +432,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 +491,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 };
|
||||
|
@ -701,7 +679,12 @@ unsigned int DomainServer::countConnectedUsers() {
|
|||
}
|
||||
|
||||
QUrl DomainServer::oauthRedirectURL() {
|
||||
return QString("https://%1:%2/oauth").arg(_hostname).arg(_httpsManager->serverPort());
|
||||
if (_httpsManager) {
|
||||
return QString("https://%1:%2/oauth").arg(_hostname).arg(_httpsManager->serverPort());
|
||||
} else {
|
||||
qWarning() << "Attempting to determine OAuth re-direct URL with no HTTPS server configured.";
|
||||
return QUrl();
|
||||
}
|
||||
}
|
||||
|
||||
const QString OAUTH_CLIENT_ID_QUERY_KEY = "client_id";
|
||||
|
@ -1082,11 +1065,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1597,10 +1645,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
|
|||
// add it to the set so we can handle the callback from the OAuth provider
|
||||
_webAuthenticationStateSet.insert(stateUUID);
|
||||
|
||||
QUrl oauthRedirectURL = oauthAuthorizationURL(stateUUID);
|
||||
QUrl authURL = oauthAuthorizationURL(stateUUID);
|
||||
|
||||
Headers redirectHeaders;
|
||||
redirectHeaders.insert("Location", oauthRedirectURL.toEncoded());
|
||||
|
||||
redirectHeaders.insert("Location", authURL.toEncoded());
|
||||
|
||||
connection->respond(HTTPConnection::StatusCode302,
|
||||
QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders);
|
||||
|
@ -1678,7 +1727,6 @@ QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenR
|
|||
return NetworkAccessManager::getInstance().get(profileRequest);
|
||||
}
|
||||
|
||||
const QString DS_SETTINGS_SESSIONS_GROUP = "web-sessions";
|
||||
Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileReply) {
|
||||
Headers cookieHeaders;
|
||||
|
||||
|
@ -1692,10 +1740,6 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR
|
|||
DomainServerWebSessionData sessionData(userObject);
|
||||
_cookieSessionHash.insert(cookieUUID, sessionData);
|
||||
|
||||
// persist the cookie to settings file so we can get it back on DS relaunch
|
||||
QStringList path = QStringList() << DS_SETTINGS_SESSIONS_GROUP << cookieUUID.toString();
|
||||
Setting::Handle<QVariant>(path).set(QVariant::fromValue(sessionData));
|
||||
|
||||
// setup expiry for cookie to 1 month from today
|
||||
QDateTime cookieExpiry = QDateTime::currentDateTimeUtc().addMonths(1);
|
||||
|
||||
|
@ -1712,18 +1756,6 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR
|
|||
return cookieHeaders;
|
||||
}
|
||||
|
||||
void DomainServer::loadExistingSessionsFromSettings() {
|
||||
// read data for existing web sessions into memory so existing sessions can be leveraged
|
||||
Settings domainServerSettings;
|
||||
domainServerSettings.beginGroup(DS_SETTINGS_SESSIONS_GROUP);
|
||||
|
||||
foreach(const QString& uuidKey, domainServerSettings.childKeys()) {
|
||||
_cookieSessionHash.insert(QUuid(uuidKey),
|
||||
domainServerSettings.value(uuidKey).value<DomainServerWebSessionData>());
|
||||
qDebug() << "Pulled web session from settings - cookie UUID is" << uuidKey;
|
||||
}
|
||||
}
|
||||
|
||||
void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment) {
|
||||
QUuid oldUUID = assignment->getUUID();
|
||||
assignment->resetUUID();
|
||||
|
@ -1970,3 +2002,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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -95,9 +95,9 @@ EntityViewer.setPosition({
|
|||
y: 0,
|
||||
z: 0
|
||||
});
|
||||
EntityViewer.setKeyholeRadius(60000);
|
||||
EntityViewer.setCenterRadius(60000);
|
||||
var octreeQueryInterval = Script.setInterval(function() {
|
||||
EntityViewer.queryOctree();
|
||||
}, 1000);
|
||||
|
||||
Script.update.connect(update);
|
||||
Script.update.connect(update);
|
||||
|
|
301
examples/airship/airship.js
Normal file
301
examples/airship/airship.js
Normal file
|
@ -0,0 +1,301 @@
|
|||
//
|
||||
// airship.js
|
||||
//
|
||||
// Animates a pirate airship that chases people and shoots cannonballs at them
|
||||
//
|
||||
// Created by Philip Rosedale on March 7, 2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
(function () {
|
||||
var entityID,
|
||||
minimumDelay = 100, // milliseconds
|
||||
distanceScale = 2.0, // distance at which 100% chance of hopping
|
||||
timeScale = 300.0, // crash
|
||||
hopStrength = 0.4, // meters / second
|
||||
spotlight = null,
|
||||
wantDebug = false,
|
||||
timeoutID = undefined,
|
||||
bullet = null,
|
||||
particles = null,
|
||||
nearbyAvatars = 0,
|
||||
nearbyAvatarsInRange = 0,
|
||||
averageAvatarLocation = { x: 0, y: 0, z: 0 },
|
||||
properties,
|
||||
lightTimer = 0,
|
||||
lightTimeoutID = undefined,
|
||||
audioInjector = null;
|
||||
|
||||
var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
|
||||
var cannonSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "philip/cannonShot.wav");
|
||||
var explosionSound = SoundCache.getSound(HIFI_PUBLIC_BUCKET + "philip/explosion.wav");
|
||||
|
||||
var NO_SHOOT_COLOR = { red: 100, green: 100, blue: 100 };
|
||||
|
||||
var MAX_TARGET_RANGE = 200;
|
||||
|
||||
function printDebug(message) {
|
||||
if (wantDebug) {
|
||||
print(message);
|
||||
}
|
||||
}
|
||||
|
||||
var LIGHT_UPDATE_INTERVAL = 50;
|
||||
var LIGHT_LIFETIME = 700;
|
||||
|
||||
function randomVector(size) {
|
||||
return { x: (Math.random() - 0.5) * size,
|
||||
y: (Math.random() - 0.5) * size,
|
||||
z: (Math.random() - 0.5) * size };
|
||||
}
|
||||
|
||||
function makeLight(parent, position, colorDivisor) {
|
||||
// Create a flickering light somewhere for a while
|
||||
if (spotlight !== null) {
|
||||
// light still exists, do nothing
|
||||
printDebug("light still there");
|
||||
return;
|
||||
}
|
||||
printDebug("making light");
|
||||
|
||||
var colorIndex = 180 + Math.random() * 50;
|
||||
|
||||
spotlight = Entities.addEntity({
|
||||
type: "Light",
|
||||
name: "Test Light",
|
||||
intensity: 50.0,
|
||||
falloffRadius: 20.0,
|
||||
dimensions: {
|
||||
x: 150,
|
||||
y: 150,
|
||||
z: 150
|
||||
},
|
||||
position: position,
|
||||
parentID: parent,
|
||||
color: {
|
||||
red: colorIndex,
|
||||
green: colorIndex / colorDivisor,
|
||||
blue: 0
|
||||
},
|
||||
lifetime: LIGHT_LIFETIME * 2
|
||||
});
|
||||
|
||||
lightTimer = 0;
|
||||
lightTimeoutID = Script.setTimeout(updateLight, LIGHT_UPDATE_INTERVAL);
|
||||
|
||||
};
|
||||
|
||||
function updateLight() {
|
||||
lightTimer += LIGHT_UPDATE_INTERVAL;
|
||||
if ((spotlight !== null) && (lightTimer > LIGHT_LIFETIME)) {
|
||||
printDebug("deleting light!");
|
||||
Entities.deleteEntity(spotlight);
|
||||
spotlight = null;
|
||||
} else {
|
||||
Entities.editEntity(spotlight, {
|
||||
intensity: 5 + Math.random() * 50,
|
||||
falloffRadius: 5 + Math.random() * 10
|
||||
});
|
||||
lightTimeoutID = Script.setTimeout(updateLight, LIGHT_UPDATE_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
function move() {
|
||||
|
||||
var HOVER_DISTANCE = 30.0;
|
||||
var RUN_TOWARD_STRENGTH = 0.002;
|
||||
var VELOCITY_FOLLOW_RATE = 0.01;
|
||||
|
||||
var range = Vec3.distance(properties.position, averageAvatarLocation);
|
||||
var impulse = { x: 0, y: 0, z: 0 };
|
||||
|
||||
// move in the XZ plane
|
||||
var away = Vec3.subtract(properties.position, averageAvatarLocation);
|
||||
away.y = 0.0;
|
||||
|
||||
if (range > HOVER_DISTANCE) {
|
||||
impulse = Vec3.multiply(-RUN_TOWARD_STRENGTH, Vec3.normalize(away));
|
||||
}
|
||||
|
||||
var rotation = Quat.rotationBetween(Vec3.UNIT_NEG_Z, properties.velocity);
|
||||
Entities.editEntity(entityID, {velocity: Vec3.sum(properties.velocity, impulse), rotation: Quat.mix(properties.rotation, rotation, VELOCITY_FOLLOW_RATE)});
|
||||
}
|
||||
|
||||
|
||||
function countAvatars() {
|
||||
var workQueue = AvatarList.getAvatarIdentifiers();
|
||||
var averageLocation = {x: 0, y: 0, z: 0};
|
||||
var summed = 0;
|
||||
var inRange = 0;
|
||||
for (var i = 0; i < workQueue.length; i++) {
|
||||
var avatar = AvatarList.getAvatar(workQueue[i]), avatarPosition = avatar && avatar.position;
|
||||
if (avatarPosition) {
|
||||
averageLocation = Vec3.sum(averageLocation, avatarPosition);
|
||||
summed++;
|
||||
if (Vec3.distance(avatarPosition, properties.position) < MAX_TARGET_RANGE) {
|
||||
inRange++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (summed > 0) {
|
||||
averageLocation = Vec3.multiply(1 / summed, averageLocation);
|
||||
}
|
||||
nearbyAvatars = summed;
|
||||
nearbyAvatarsInRange = inRange;
|
||||
averageAvatarLocation = averageLocation;
|
||||
|
||||
//printDebug(" Avatars: " + summed + "in range: " + nearbyAvatarsInRange);
|
||||
//Vec3.print(" location: ", averageAvatarLocation);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function shoot() {
|
||||
if (bullet !== null) {
|
||||
return;
|
||||
}
|
||||
if (Vec3.distance(MyAvatar.position, properties.position) > MAX_TARGET_RANGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
var SPEED = 7.5;
|
||||
var GRAVITY = -2.0;
|
||||
var DIAMETER = 0.5;
|
||||
var direction = Vec3.subtract(MyAvatar.position, properties.position);
|
||||
var range = Vec3.distance(MyAvatar.position, properties.position);
|
||||
var timeOfFlight = range / SPEED;
|
||||
|
||||
var fall = 0.5 * -GRAVITY * (timeOfFlight * timeOfFlight);
|
||||
|
||||
var velocity = Vec3.multiply(SPEED, Vec3.normalize(direction));
|
||||
velocity.y += 0.5 * fall / timeOfFlight * 2.0;
|
||||
|
||||
var DISTANCE_TO_DECK = 3;
|
||||
var bulletStart = properties.position;
|
||||
bulletStart.y -= DISTANCE_TO_DECK;
|
||||
|
||||
makeLight(entityID, Vec3.sum(properties.position, randomVector(10.0)), 2);
|
||||
|
||||
bullet = Entities.addEntity({
|
||||
type: "Sphere",
|
||||
name: "cannonball",
|
||||
position: properties.position,
|
||||
dimensions: { x: DIAMETER, y: DIAMETER, z: DIAMETER },
|
||||
color: { red: 10, green: 10, blue: 10 },
|
||||
velocity: velocity,
|
||||
damping: 0.01,
|
||||
dynamic: true,
|
||||
ignoreForCollisions: true,
|
||||
gravity: { x: 0, y: GRAVITY, z: 0 },
|
||||
lifetime: timeOfFlight * 2
|
||||
});
|
||||
|
||||
Audio.playSound(cannonSound, {
|
||||
position: Vec3.sum(MyAvatar.position, velocity),
|
||||
volume: 1.0
|
||||
});
|
||||
makeParticles(properties.position, bullet, timeOfFlight * 2);
|
||||
Script.setTimeout(explode, timeOfFlight * 1000);
|
||||
}
|
||||
|
||||
function explode() {
|
||||
var properties = Entities.getEntityProperties(bullet);
|
||||
var direction = Vec3.normalize(Vec3.subtract(MyAvatar.position, properties.position));
|
||||
makeLight(null, properties.position, 10);
|
||||
Audio.playSound(explosionSound, { position: Vec3.sum(MyAvatar.position, direction), volume: 1.0 });
|
||||
bullet = null;
|
||||
}
|
||||
|
||||
|
||||
function maybe() { // every user checks their distance and tries to claim if close enough.
|
||||
var PROBABILITY_OF_SHOOT = 0.015;
|
||||
properties = Entities.getEntityProperties(entityID);
|
||||
countAvatars();
|
||||
|
||||
if (nearbyAvatars && (Math.random() < 1 / nearbyAvatars)) {
|
||||
move();
|
||||
}
|
||||
|
||||
if (nearbyAvatarsInRange && (Math.random() < PROBABILITY_OF_SHOOT / nearbyAvatarsInRange)) {
|
||||
shoot();
|
||||
}
|
||||
|
||||
var TIME_TO_NEXT_CHECK = 33;
|
||||
timeoutID = Script.setTimeout(maybe, TIME_TO_NEXT_CHECK);
|
||||
}
|
||||
|
||||
this.preload = function (givenEntityID) {
|
||||
printDebug("preload airship v1...");
|
||||
|
||||
entityID = givenEntityID;
|
||||
properties = Entities.getEntityProperties(entityID);
|
||||
timeoutID = Script.setTimeout(maybe, minimumDelay);
|
||||
};
|
||||
|
||||
this.unload = function () {
|
||||
printDebug("unload airship...");
|
||||
if (timeoutID !== undefined) {
|
||||
Script.clearTimeout(timeoutID);
|
||||
}
|
||||
if (lightTimeoutID !== undefined) {
|
||||
Script.clearTimeout(lightTimeoutID);
|
||||
}
|
||||
if (spotlight !== null) {
|
||||
Entities.deleteEntity(spotlight);
|
||||
}
|
||||
};
|
||||
|
||||
function makeParticles(position, parent, lifespan) {
|
||||
particles = Entities.addEntity({
|
||||
type: 'ParticleEffect',
|
||||
position: position,
|
||||
parentID: parent,
|
||||
color: {
|
||||
red: 70,
|
||||
green: 70,
|
||||
blue: 70
|
||||
},
|
||||
isEmitting: 1,
|
||||
maxParticles: 1000,
|
||||
lifetime: lifespan,
|
||||
lifespan: lifespan / 3,
|
||||
emitRate: 80,
|
||||
emitSpeed: 0,
|
||||
speedSpread: 1.0,
|
||||
emitRadiusStart: 1,
|
||||
polarStart: -Math.PI/8,
|
||||
polarFinish: Math.PI/8,
|
||||
azimuthStart: -Math.PI/4,
|
||||
azimuthFinish: Math.PI/4,
|
||||
emitAcceleration: { x: 0, y: 0, z: 0 },
|
||||
particleRadius: 0.25,
|
||||
radiusSpread: 0.1,
|
||||
radiusStart: 0.3,
|
||||
radiusFinish: 0.15,
|
||||
colorSpread: {
|
||||
red: 100,
|
||||
green: 100,
|
||||
blue: 0
|
||||
},
|
||||
colorStart: {
|
||||
red: 125,
|
||||
green: 125,
|
||||
blue: 125
|
||||
},
|
||||
colorFinish: {
|
||||
red: 10,
|
||||
green: 10,
|
||||
blue: 10
|
||||
},
|
||||
alpha: 0.5,
|
||||
alphaSpread: 0,
|
||||
alphaStart: 1,
|
||||
alphaFinish: 0,
|
||||
emitterShouldTrail: true,
|
||||
textures: 'https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png'
|
||||
});
|
||||
}
|
||||
})
|
59
examples/airship/makeAirship.js
Normal file
59
examples/airship/makeAirship.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// makeAirship.js
|
||||
//
|
||||
// // Created by Philip Rosedale on March 7, 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 SIZE = 0.2;
|
||||
var TYPE = "Model"; // Right now this can be "Box" or "Model" or "Sphere"
|
||||
|
||||
var MODEL_URL = "https://s3.amazonaws.com/hifi-public/philip/airship_compact.fbx"
|
||||
|
||||
var MODEL_DIMENSION = { x: 19.257, y: 24.094, z: 40.3122 };
|
||||
var ENTITY_URL = "https://s3.amazonaws.com/hifi-public/scripts/airship/airship.js";
|
||||
|
||||
|
||||
var LIFETIME = 3600 * 48;
|
||||
|
||||
var GRAVITY = { x: 0, y: 0, z: 0 };
|
||||
var DAMPING = 0.05;
|
||||
var ANGULAR_DAMPING = 0.01;
|
||||
|
||||
var collidable = true;
|
||||
var gravity = true;
|
||||
|
||||
var HOW_FAR_IN_FRONT_OF_ME = 30;
|
||||
var HOW_FAR_ABOVE_ME = 15;
|
||||
|
||||
var leaveBehind = true;
|
||||
|
||||
var shipLocation = Vec3.sum(MyAvatar.position, Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getFront(Camera.orientation)));
|
||||
shipLocation.y += HOW_FAR_ABOVE_ME;
|
||||
|
||||
|
||||
var airship = Entities.addEntity({
|
||||
type: TYPE,
|
||||
modelURL: MODEL_URL,
|
||||
name: "airship",
|
||||
position: shipLocation,
|
||||
dimensions: (TYPE == "Model") ? MODEL_DIMENSION : { x: SIZE, y: SIZE, z: SIZE },
|
||||
damping: DAMPING,
|
||||
angularDamping: ANGULAR_DAMPING,
|
||||
gravity: (gravity ? GRAVITY : { x: 0, y: 0, z: 0}),
|
||||
dynamic: collidable,
|
||||
lifetime: LIFETIME,
|
||||
animation: {url: MODEL_URL, running: true, currentFrame: 0, loop: true},
|
||||
script: ENTITY_URL
|
||||
});
|
||||
|
||||
function scriptEnding() {
|
||||
if (!leaveBehind) {
|
||||
Entities.deleteEntity(airship);
|
||||
}
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(scriptEnding);
|
|
@ -213,7 +213,6 @@ function AttachedEntitiesManager() {
|
|||
var props = Entities.getEntityProperties(entityID);
|
||||
if (props.parentID == MyAvatar.sessionUUID) {
|
||||
grabData = getEntityCustomData('grabKey', entityID, {});
|
||||
grabbableData = getEntityCustomData('grabbableKey', entityID, {});
|
||||
var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA);
|
||||
var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex];
|
||||
wearableData.joints[currentJointName] = [props.localPosition, props.localRotation];
|
||||
|
|
|
@ -1,218 +1,260 @@
|
|||
"use strict";
|
||||
/*jslint nomen: true, plusplus: true, vars: true*/
|
||||
/*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/
|
||||
//
|
||||
// ACAudioSearchAndInject.js
|
||||
// audio
|
||||
//
|
||||
// Created by Eric Levin 2/1/2016
|
||||
// Created by Eric Levin and Howard Stearns 2/1/2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
|
||||
// This AC script searches for special sound entities nearby avatars and plays those sounds based off information specified in the entity's
|
||||
// user data field ( see acAudioSearchAndCompatibilityEntitySpawner.js for an example)
|
||||
//
|
||||
// Keeps track of all sounds within QUERY_RADIUS of an avatar, where a "sound" is specified in entity userData.
|
||||
// Inject as many as practical into the audio mixer.
|
||||
// See acAudioSearchAndCompatibilityEntitySpawner.js.
|
||||
//
|
||||
// This implementation takes some precautions to scale well:
|
||||
// - It doesn't hastle the entity server because it issues at most one octree query every UPDATE_TIME period, regardless of the number of avatars.
|
||||
// - It does not load itself because it only gathers entities once every UPDATE_TIME period, and only
|
||||
// checks entity properties for those small number of entities that are currently playing (plus a RECHECK_TIME period examination of all entities).
|
||||
// This implementation tries to use all the available injectors.
|
||||
//
|
||||
// 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("https://rawgit.com/highfidelity/hifi/master/examples/libraries/utils.js");
|
||||
|
||||
var SOUND_DATA_KEY = "soundKey";
|
||||
|
||||
var QUERY_RADIUS = 50;
|
||||
|
||||
EntityViewer.setKeyholeRadius(QUERY_RADIUS);
|
||||
Entities.setPacketsPerSecond(6000);
|
||||
|
||||
Agent.isAvatar = true;
|
||||
var MSEC_PER_SEC = 1000;
|
||||
var SOUND_DATA_KEY = "io.highfidelity.soundKey"; // Sound data is specified in userData under this key.
|
||||
var old_sound_data_key = "soundKey"; // For backwards compatibility.
|
||||
var QUERY_RADIUS = 50; // meters
|
||||
var UPDATE_TIME = 100; // ms. We'll update just one thing on this period.
|
||||
var EXPIRATION_TIME = 5 * MSEC_PER_SEC; // ms. Remove sounds that have been out of range for this time.
|
||||
var RECHECK_TIME = 10 * MSEC_PER_SEC; // ms. Check for new userData properties this often when not currently playing.
|
||||
// (By not checking most of the time when not playing, we can efficiently go through all entities without getEntityProperties.)
|
||||
var UPDATES_PER_STATS_LOG = RECHECK_TIME / UPDATE_TIME; // (It's nice to smooth out the results by straddling a recheck.)
|
||||
|
||||
var DEFAULT_SOUND_DATA = {
|
||||
volume: 0.5,
|
||||
loop: false,
|
||||
playbackGap: 1000, // in ms
|
||||
volume: 0.5, // userData cannot specify zero volume with our current method of defaulting.
|
||||
loop: false, // Default must be false with our current method of defaulting, else there's no way to get a false value.
|
||||
playbackGap: MSEC_PER_SEC, // in ms
|
||||
playbackGapRange: 0 // in ms
|
||||
};
|
||||
var MIN_PLAYBACK_GAP = 0;
|
||||
|
||||
var UPDATE_TIME = 100;
|
||||
var EXPIRATION_TIME = 5000;
|
||||
Script.include("../../libraries/utils.js");
|
||||
Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList.
|
||||
function ignore() {}
|
||||
function debug() { // Display the arguments not just [Object object].
|
||||
//print.apply(null, [].map.call(arguments, JSON.stringify));
|
||||
}
|
||||
|
||||
var soundEntityMap = {};
|
||||
var soundUrls = {};
|
||||
EntityViewer.setCenterRadius(QUERY_RADIUS);
|
||||
|
||||
var avatarPositions = [];
|
||||
|
||||
|
||||
function update() {
|
||||
var avatars = AvatarList.getAvatarIdentifiers();
|
||||
for (var i = 0; i < avatars.length; i++) {
|
||||
var avatar = AvatarList.getAvatar(avatars[i]);
|
||||
var avatarPosition = avatar.position;
|
||||
if (!avatarPosition) {
|
||||
continue;
|
||||
// ENTITY DATA CACHE
|
||||
//
|
||||
var entityCache = {}; // A dictionary of unexpired EntityData objects.
|
||||
var examinationCount = 0;
|
||||
function EntityDatum(entityIdentifier) { // Just the data of an entity that we need to know about.
|
||||
// This data is only use for our sound injection. There is no need to store such info in the replicated entity on everyone's computer.
|
||||
var that = this;
|
||||
that.lastUserDataUpdate = 0; // new entity is in need of rechecking user data
|
||||
// State Transitions:
|
||||
// no data => no data | sound data | expired
|
||||
// expired => stop => remove
|
||||
// sound data => downloading
|
||||
// downloading => downloading | waiting
|
||||
// waiting => playing | waiting (if too many already playing)
|
||||
// playing => update position etc | no data
|
||||
that.stop = function stop() {
|
||||
if (!that.sound) {
|
||||
return;
|
||||
}
|
||||
EntityViewer.setPosition(avatarPosition);
|
||||
EntityViewer.queryOctree();
|
||||
avatarPositions.push(avatarPosition);
|
||||
print("stopping sound", entityIdentifier, that.url);
|
||||
delete that.sound;
|
||||
delete that.url;
|
||||
if (!that.injector) {
|
||||
return;
|
||||
}
|
||||
that.injector.stop();
|
||||
delete that.injector;
|
||||
};
|
||||
this.update = function stateTransitions(expirationCutoff, userDataCutoff, now) {
|
||||
if (that.timestamp < expirationCutoff) { // EXPIRED => STOP => REMOVE
|
||||
that.stop(); // Alternatively, we could fade out and then stop...
|
||||
delete entityCache[entityIdentifier];
|
||||
return;
|
||||
}
|
||||
var properties, soundData; // Latest data, pulled from local octree.
|
||||
// getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results.
|
||||
// Most entity updates are fast and only a very few do getEntityProperties.
|
||||
function ensureSoundData() { // We only getEntityProperities when we need to.
|
||||
if (properties) {
|
||||
return;
|
||||
}
|
||||
properties = Entities.getEntityProperties(entityIdentifier, ['userData', 'position']);
|
||||
examinationCount++; // Collect statistics on how many getEntityProperties we do.
|
||||
debug("updating", that, properties);
|
||||
try {
|
||||
var userData = properties.userData && JSON.parse(properties.userData);
|
||||
soundData = userData && (userData[SOUND_DATA_KEY] || userData[old_sound_data_key]); // Don't store soundData yet. Let state changes compare.
|
||||
that.lastUserDataUpdate = now; // But do update these ...
|
||||
that.url = soundData && soundData.url;
|
||||
that.playAfter = that.url && now;
|
||||
} catch (err) {
|
||||
print(err, properties.userData);
|
||||
}
|
||||
}
|
||||
// Stumbling on big new pile of entities will do a lot of getEntityProperties. Once.
|
||||
if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA
|
||||
ensureSoundData();
|
||||
}
|
||||
if (!that.url) { // NO DATA => NO DATA
|
||||
return that.stop();
|
||||
}
|
||||
if (!that.sound) { // SOUND DATA => DOWNLOADING
|
||||
that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can.
|
||||
}
|
||||
if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING
|
||||
return;
|
||||
}
|
||||
if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING
|
||||
return;
|
||||
}
|
||||
ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too.
|
||||
if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around)
|
||||
return that.stop();
|
||||
}
|
||||
var options = {
|
||||
position: properties.position,
|
||||
loop: soundData.loop || DEFAULT_SOUND_DATA.loop,
|
||||
volume: soundData.volume || DEFAULT_SOUND_DATA.volume
|
||||
};
|
||||
function repeat() {
|
||||
return !options.loop && (soundData.playbackGap >= 0);
|
||||
}
|
||||
function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work
|
||||
var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange,
|
||||
base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME;
|
||||
return now + base + randFloat(-Math.min(base, range), range);
|
||||
}
|
||||
if (!that.injector) { // WAITING => PLAYING | WAITING
|
||||
debug("starting", that, options);
|
||||
that.injector = Audio.playSound(that.sound, options); // Might be null if at at injector limit. Will try again later.
|
||||
if (that.injector) {
|
||||
print("started", entityIdentifier, that.url);
|
||||
} else { // Don't hammer ensureSoundData or injector manager.
|
||||
that.playAfter = randomizedNextPlay();
|
||||
}
|
||||
return;
|
||||
}
|
||||
that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC
|
||||
if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap.
|
||||
if (repeat()) { // WAITING => PLAYING
|
||||
// Setup next play just once, now. Changes won't be looked at while we wait.
|
||||
that.playAfter = randomizedNextPlay();
|
||||
// Subtle: if the restart fails b/c we're at injector limit, we won't try again until next playAfter.
|
||||
that.injector.restart();
|
||||
} else { // PLAYING => NO DATA
|
||||
that.playAfter = Infinity; // was one-shot and we're finished
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) {
|
||||
ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play.
|
||||
var entitySound = entityCache[entityIdentifier];
|
||||
if (!entitySound) {
|
||||
entitySound = entityCache[entityIdentifier] = new EntityDatum(entityIdentifier);
|
||||
}
|
||||
Script.setTimeout(function() {
|
||||
avatarPositions.forEach(function(avatarPosition) {
|
||||
var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS);
|
||||
handleFoundSoundEntities(entities);
|
||||
entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine.
|
||||
}
|
||||
var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now();
|
||||
function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds.
|
||||
var now = Date.now(),
|
||||
expirationCutoff = now - EXPIRATION_TIME,
|
||||
userDataRecheckCutoff = now - RECHECK_TIME;
|
||||
Object.keys(entityCache).forEach(function (entityIdentifier) {
|
||||
entityCache[entityIdentifier].update(expirationCutoff, userDataRecheckCutoff, now);
|
||||
});
|
||||
if (nUpdates-- <= 0) { // Report statistics.
|
||||
// For example, with:
|
||||
// injector-limit = 40 (in C++ code)
|
||||
// N_SOUNDS = 1000 (from userData in, e.g., acAudioSearchCompatibleEntitySpawner.js)
|
||||
// replay-period = 3 + 20 = 23 (seconds, ditto)
|
||||
// stats-period = UPDATES_PER_STATS_LOG * UPDATE_TIME / MSEC_PER_SEC = 10 seconds
|
||||
// The log should show between each stats report:
|
||||
// "start" lines ~= injector-limit * P(finish) = injector-limit * stats-period/replay-period = 17 ?
|
||||
// total attempts at starting ("start" lines + "could not thread" lines) ~= N_SOUNDS = 1000 ?
|
||||
// entities > N_SOUNDS * (1+ N_SILENT_ENTITIES_PER_SOUND) = 11000 + whatever was in the scene before running spawner
|
||||
// sounds = N_SOUNDS = 1000
|
||||
// getEntityPropertiesPerUpdate ~= playing + failed-starts/UPDATES_PER_STATS_LOG + other-rechecks-each-update
|
||||
// = injector-limit + (total attempts - "start" lines)/UPDATES_PER_STATS__LOG
|
||||
// + (entities - playing - failed-starts/UPDATES_PER_STATS_LOG) * P(recheck-in-update)
|
||||
// where failed-starts/UPDATES_PER_STATS_LOG = (1000-17)/100 = 10
|
||||
// = 40 + 10 + (11000 - 40 - 10)*UPDATE_TIME/RECHECK_TIME
|
||||
// = 40 + 10 + 10950*0.01 = 159 (mostly proportional to enties/RECHECK_TIME)
|
||||
// millisecondsPerUpdate ~= UPDATE_TIME = 100 (+ some timer machinery time)
|
||||
// this assignment client activity monitor < 100% cpu
|
||||
var stats = {
|
||||
entities: 0,
|
||||
sounds: 0,
|
||||
playing: 0,
|
||||
getEntityPropertiesPerUpdate: examinationCount / UPDATES_PER_STATS_LOG,
|
||||
millisecondsPerUpdate: (now - lastStats) / UPDATES_PER_STATS_LOG
|
||||
};
|
||||
nUpdates = UPDATES_PER_STATS_LOG;
|
||||
lastStats = now;
|
||||
examinationCount = 0;
|
||||
Object.keys(entityCache).forEach(function (entityIdentifier) {
|
||||
var datum = entityCache[entityIdentifier];
|
||||
stats.entities++;
|
||||
if (datum.url) {
|
||||
stats.sounds++;
|
||||
if (datum.injector && datum.injector.isPlaying) {
|
||||
stats.playing++;
|
||||
}
|
||||
}
|
||||
});
|
||||
//Now wipe list for next query;
|
||||
avatarPositions = [];
|
||||
}, UPDATE_TIME);
|
||||
handleActiveSoundEntities();
|
||||
}
|
||||
|
||||
function handleActiveSoundEntities() {
|
||||
// Go through all our sound entities, if they have passed expiration time, remove them from map
|
||||
for (var potentialSoundEntity in soundEntityMap) {
|
||||
if (!soundEntityMap.hasOwnProperty(potentialSoundEntity)) {
|
||||
// The current property is not a direct property of soundEntityMap so ignore it
|
||||
continue;
|
||||
}
|
||||
var soundEntity = potentialSoundEntity;
|
||||
var soundProperties = soundEntityMap[soundEntity];
|
||||
soundProperties.timeWithoutAvatarInRange += UPDATE_TIME;
|
||||
if (soundProperties.timeWithoutAvatarInRange > EXPIRATION_TIME && soundProperties.soundInjector) {
|
||||
// An avatar hasn't been within range of this sound entity recently, so remove it from map
|
||||
soundProperties.soundInjector.stop();
|
||||
delete soundEntityMap[soundEntity];
|
||||
} else if (soundProperties.isDownloaded) {
|
||||
// If this sound hasn't expired yet, we want to potentially play it!
|
||||
if (soundProperties.readyToPlay) {
|
||||
var newPosition = Entities.getEntityProperties(soundEntity, "position").position;
|
||||
if (!soundProperties.soundInjector) {
|
||||
soundProperties.soundInjector = Audio.playSound(soundProperties.sound, {
|
||||
volume: soundProperties.volume,
|
||||
position: newPosition,
|
||||
loop: soundProperties.loop
|
||||
});
|
||||
} else {
|
||||
soundProperties.soundInjector.restart();
|
||||
}
|
||||
soundProperties.readyToPlay = false;
|
||||
} else if (soundProperties.sound && soundProperties.loop === false) {
|
||||
// We need to check all of our entities that are not looping but have an interval associated with them
|
||||
// to see if it's time for them to play again
|
||||
soundProperties.timeSinceLastPlay += UPDATE_TIME;
|
||||
if (soundProperties.timeSinceLastPlay > soundProperties.clipDuration + soundProperties.currentPlaybackGap) {
|
||||
soundProperties.readyToPlay = true;
|
||||
soundProperties.timeSinceLastPlay = 0;
|
||||
// Now let's get our new current interval
|
||||
soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange);
|
||||
soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap);
|
||||
}
|
||||
}
|
||||
}
|
||||
print(JSON.stringify(stats));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleFoundSoundEntities(entities) {
|
||||
entities.forEach(function(entity) {
|
||||
var soundData = getEntityCustomData(SOUND_DATA_KEY, entity);
|
||||
if (soundData && soundData.url) {
|
||||
//check sound entities list- if it's not in, add it
|
||||
if (!soundEntityMap[entity]) {
|
||||
var soundProperties = {
|
||||
url: soundData.url,
|
||||
volume: soundData.volume || DEFAULT_SOUND_DATA.volume,
|
||||
loop: soundData.loop || DEFAULT_SOUND_DATA.loop,
|
||||
playbackGap: soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap,
|
||||
playbackGapRange: soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange,
|
||||
readyToPlay: false,
|
||||
position: Entities.getEntityProperties(entity, "position").position,
|
||||
timeSinceLastPlay: 0,
|
||||
timeWithoutAvatarInRange: 0,
|
||||
isDownloaded: false
|
||||
};
|
||||
|
||||
|
||||
soundProperties.currentPlaybackGap = soundProperties.playbackGap + randFloat(-soundProperties.playbackGapRange, soundProperties.playbackGapRange);
|
||||
soundProperties.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, soundProperties.currentPlaybackGap);
|
||||
|
||||
|
||||
soundEntityMap[entity] = soundProperties;
|
||||
if (!soundUrls[soundData.url]) {
|
||||
// We need to download sound before we add it to our map
|
||||
var sound = SoundCache.getSound(soundData.url);
|
||||
// Only add it to map once it's downloaded
|
||||
soundUrls[soundData.url] = sound;
|
||||
sound.ready.connect(function() {
|
||||
soundProperties.sound = sound;
|
||||
soundProperties.readyToPlay = true;
|
||||
soundProperties.isDownloaded = true;
|
||||
soundProperties.clipDuration = sound.duration * 1000;
|
||||
soundEntityMap[entity] = soundProperties;
|
||||
|
||||
});
|
||||
} else {
|
||||
// We already have sound downloaded, so just add it to map right away
|
||||
soundProperties.sound = soundUrls[soundData.url];
|
||||
soundProperties.clipDuration = soundProperties.sound.duration * 1000;
|
||||
soundProperties.readyToPlay = true;
|
||||
soundProperties.isDownloaded = true;
|
||||
soundEntityMap[entity] = soundProperties;
|
||||
}
|
||||
} else {
|
||||
//If this sound is in our map already, we want to reset timeWithoutAvatarInRange
|
||||
// Also we want to check to see if the entity has been updated with new sound data- if so we want to update!
|
||||
soundEntityMap[entity].timeWithoutAvatarInRange = 0;
|
||||
checkForSoundPropertyChanges(soundEntityMap[entity], soundData);
|
||||
}
|
||||
}
|
||||
// Update the set of which EntityData we know about.
|
||||
//
|
||||
function updateEntiesForAvatar(avatarIdentifier) { // Just one piece of update work.
|
||||
// This does at most:
|
||||
// one queryOctree request of the entity server, and
|
||||
// one findEntities geometry query of our own octree, and
|
||||
// a quick internEntityDatum of each of what may be a large number of entityIdentifiers.
|
||||
// The idea is that this is a nice bounded piece of work that should not be done too frequently.
|
||||
// However, it means that we won't learn about new entities until, on average (nAvatars * UPDATE_TIME) + query round trip.
|
||||
var avatar = AvatarList.getAvatar(avatarIdentifier), avatarPosition = avatar && avatar.position;
|
||||
if (!avatarPosition) { // No longer here.
|
||||
return;
|
||||
}
|
||||
var timestamp = Date.now();
|
||||
EntityViewer.setPosition(avatarPosition);
|
||||
EntityViewer.queryOctree(); // Requests an update, but there's no telling when we'll actually see different results.
|
||||
var entities = Entities.findEntities(avatarPosition, QUERY_RADIUS);
|
||||
debug("found", entities.length, "entities near", avatar.name || "unknown", "at", avatarPosition);
|
||||
entities.forEach(function (entityIdentifier) {
|
||||
internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar);
|
||||
});
|
||||
}
|
||||
|
||||
function checkForSoundPropertyChanges(currentProps, newProps) {
|
||||
var needsNewInjector = false;
|
||||
|
||||
if (currentProps.playbackGap !== newProps.playbackGap && !currentProps.loop) {
|
||||
// playbackGap only applies to non looping sounds
|
||||
currentProps.playbackGap = newProps.playbackGap;
|
||||
currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange);
|
||||
currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap);
|
||||
currentProps.readyToPlay = true;
|
||||
}
|
||||
|
||||
if (currentProps.playbackGapRange !== currentProps.playbackGapRange) {
|
||||
currentProps.playbackGapRange = newProps.playbackGapRange;
|
||||
currentProps.currentPlaybackGap = currentProps.playbackGap + randFloat(-currentProps.playbackGapRange, currentProps.playbackGapRange);
|
||||
currentProps.currentPlaybackGap = Math.max(MIN_PLAYBACK_GAP, currentProps.currentPlaybackGap);
|
||||
currentProps.readyToPlay = true;
|
||||
}
|
||||
if (currentProps.volume !== newProps.volume) {
|
||||
currentProps.volume = newProps.volume;
|
||||
needsNewInjector = true;
|
||||
}
|
||||
if (currentProps.url !== newProps.url) {
|
||||
currentProps.url = newProps.url;
|
||||
currentProps.sound = null;
|
||||
if (!soundUrls[currentProps.url]) {
|
||||
var sound = SoundCache.getSound(currentProps.url);
|
||||
currentProps.isDownloaded = false;
|
||||
sound.ready.connect(function() {
|
||||
currentProps.sound = sound;
|
||||
currentProps.clipDuration = sound.duration * 1000;
|
||||
currentProps.isDownloaded = true;
|
||||
});
|
||||
} else {
|
||||
currentProps.sound = sound;
|
||||
currentProps.clipDuration = sound.duration * 1000;
|
||||
}
|
||||
needsNewInjector = true;
|
||||
}
|
||||
|
||||
if (currentProps.loop !== newProps.loop) {
|
||||
currentProps.loop = newProps.loop;
|
||||
needsNewInjector = true;
|
||||
}
|
||||
if (needsNewInjector) {
|
||||
// If we were looping we need to stop that so new changes are applied
|
||||
currentProps.soundInjector.stop();
|
||||
currentProps.soundInjector = null;
|
||||
currentProps.readyToPlay = true;
|
||||
}
|
||||
|
||||
// Slowly update the set of data we have to work with.
|
||||
//
|
||||
var workQueue = [];
|
||||
function updateWorkQueueForAvatarsPresent() { // when nothing else to do, fill queue with individual avatar updates
|
||||
workQueue = AvatarList.getAvatarIdentifiers().map(function (avatarIdentifier) {
|
||||
return function () {
|
||||
updateEntiesForAvatar(avatarIdentifier);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Script.setInterval(update, UPDATE_TIME);
|
||||
Script.setInterval(function () {
|
||||
// There might be thousands of EntityData known to us, but only a few will require any work to update.
|
||||
updateAllEntityData(); // i.e., this better be pretty fast.
|
||||
// Each interval, we do no more than one updateEntitiesforAvatar.
|
||||
if (!workQueue.length) {
|
||||
workQueue = [updateWorkQueueForAvatarsPresent];
|
||||
}
|
||||
workQueue.pop()(); // There's always one
|
||||
}, UPDATE_TIME);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//
|
||||
"use strict";
|
||||
/*jslint nomen: true, plusplus: true, vars: true*/
|
||||
/*global Entities, Script, Quat, Vec3, Camera, MyAvatar, print, randFloat*/
|
||||
// acAudioSearchCompatibleEntitySpawner.js
|
||||
// audio/acAudioSearching
|
||||
//
|
||||
|
@ -13,6 +15,10 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var N_SOUNDS = 1000;
|
||||
var N_SILENT_ENTITIES_PER_SOUND = 10;
|
||||
var ADD_PERIOD = 50; // ms between adding 1 sound + N_SILENT_ENTITIES_PER_SOUND, to not overrun entity server.
|
||||
var SPATIAL_DISTRIBUTION = 10; // meters spread over how far to randomly distribute enties.
|
||||
Script.include("../../libraries/utils.js");
|
||||
var orientation = Camera.getOrientation();
|
||||
orientation = Quat.safeEulerAngles(orientation);
|
||||
|
@ -20,20 +26,21 @@ orientation.x = 0;
|
|||
orientation = Quat.fromVec3Degrees(orientation);
|
||||
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
|
||||
// http://hifi-public.s3.amazonaws.com/ryan/demo/0619_Fireplace__Tree_B.L.wav
|
||||
var SOUND_DATA_KEY = "soundKey";
|
||||
var SOUND_DATA_KEY = "io.highfidelity.soundKey";
|
||||
var userData = {
|
||||
soundKey: {
|
||||
url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Sounds/ClothSail/cloth_sail3.L.wav",
|
||||
volume: 0.3,
|
||||
loop: false,
|
||||
playbackGap: 2000, // In ms - time to wait in between clip plays
|
||||
playbackGapRange: 500 // In ms - the range to wait in between clip plays
|
||||
playbackGap: 20000, // In ms - time to wait in between clip plays
|
||||
playbackGapRange: 5000 // In ms - the range to wait in between clip plays
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var userDataString = JSON.stringify(userData);
|
||||
var entityProps = {
|
||||
type: "Box",
|
||||
position: center,
|
||||
name: 'audioSearchEntity',
|
||||
color: {
|
||||
red: 200,
|
||||
green: 10,
|
||||
|
@ -43,15 +50,41 @@ var entityProps = {
|
|||
x: 0.1,
|
||||
y: 0.1,
|
||||
z: 0.1
|
||||
},
|
||||
userData: JSON.stringify(userData)
|
||||
}
|
||||
};
|
||||
|
||||
var entities = [], nSounds = 0;
|
||||
Script.include("../../libraries/utils.js");
|
||||
function addOneSet() {
|
||||
function randomizeDimension(coordinate) {
|
||||
return coordinate + randFloat(-SPATIAL_DISTRIBUTION / 2, SPATIAL_DISTRIBUTION / 2);
|
||||
}
|
||||
function randomize() {
|
||||
return {x: randomizeDimension(center.x), y: randomizeDimension(center.y), z: randomizeDimension(center.z)};
|
||||
}
|
||||
function addOne() {
|
||||
entityProps.position = randomize();
|
||||
entities.push(Entities.addEntity(entityProps));
|
||||
}
|
||||
var i;
|
||||
entityProps.userData = userDataString;
|
||||
entityProps.color.red = 200;
|
||||
addOne();
|
||||
delete entityProps.userData;
|
||||
entityProps.color.red = 10;
|
||||
for (i = 0; i < N_SILENT_ENTITIES_PER_SOUND; i++) {
|
||||
addOne();
|
||||
}
|
||||
if (++nSounds < N_SOUNDS) {
|
||||
Script.setTimeout(addOneSet, ADD_PERIOD);
|
||||
}
|
||||
}
|
||||
|
||||
var soundEntity = Entities.addEntity(entityProps);
|
||||
|
||||
addOneSet();
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(soundEntity);
|
||||
entities.forEach(Entities.deleteEntity);
|
||||
}
|
||||
// In console:
|
||||
// Entities.findEntities(MyAvatar.position, 100).forEach(function (id) { if (Entities.getEntityProperties(id).name === 'audioSearchEntity') Entities.deleteEntity(id); })
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
|
152
examples/away.js
152
examples/away.js
|
@ -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,35 +81,90 @@ 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;
|
||||
var wasOverlaysVisible = Menu.isOptionChecked("Overlays");
|
||||
var eventMappingName = "io.highfidelity.away"; // goActive on hand controller button events, too.
|
||||
var eventMapping = Controller.newMapping(eventMappingName);
|
||||
|
||||
// backward compatible version of getting HMD.mounted, so it works in old clients
|
||||
function safeGetHMDMounted() {
|
||||
if (HMD.mounted === undefined) {
|
||||
return true;
|
||||
}
|
||||
return HMD.mounted;
|
||||
}
|
||||
var wasHmdMounted = safeGetHMDMounted();
|
||||
|
||||
function goAway() {
|
||||
if (isAway) {
|
||||
return;
|
||||
|
@ -106,7 +178,21 @@ function goAway() {
|
|||
MyAvatar.setEnableMeshVisible(false); // just for our own display, without changing point of view
|
||||
playAwayAnimation(); // animation is still seen by others
|
||||
showOverlay();
|
||||
|
||||
// remember the View > Overlays state...
|
||||
wasOverlaysVisible = Menu.isOptionChecked("Overlays");
|
||||
|
||||
// show overlays so that people can see the "Away" message
|
||||
Menu.setIsOptionChecked("Overlays", true);
|
||||
|
||||
// tell the Reticle, we want to stop capturing the mouse until we come back
|
||||
Reticle.allowMouseCapture = false;
|
||||
if (HMD.active) {
|
||||
Reticle.visible = false;
|
||||
}
|
||||
wasHmdMounted = safeGetHMDMounted(); // always remember the correct state
|
||||
}
|
||||
|
||||
function goActive() {
|
||||
if (!isAway) {
|
||||
return;
|
||||
|
@ -119,19 +205,33 @@ function goActive() {
|
|||
MyAvatar.setEnableMeshVisible(true); // IWBNI we respected Developer->Avatar->Draw Mesh setting.
|
||||
stopAwayAnimation();
|
||||
hideOverlay();
|
||||
|
||||
// restore overlays state to what it was when we went "away"
|
||||
Menu.setIsOptionChecked("Overlays", wasOverlaysVisible);
|
||||
|
||||
// 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();
|
||||
}
|
||||
wasHmdMounted = safeGetHMDMounted(); // always remember the correct state
|
||||
}
|
||||
|
||||
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 wasHmdActive = HMD.active;
|
||||
var wasMouseCaptured = Reticle.mouseCaptured;
|
||||
|
||||
function maybeGoAway() {
|
||||
if (HMD.active !== wasHmdActive) {
|
||||
wasHmdActive = !wasHmdActive;
|
||||
|
@ -139,8 +239,26 @@ function maybeGoAway() {
|
|||
goAway();
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
goAway();
|
||||
}
|
||||
}
|
||||
|
||||
// If you've removed your HMD from your head, and we can detect it, we will also go away...
|
||||
var hmdMounted = safeGetHMDMounted();
|
||||
if (HMD.active && !hmdMounted && wasHmdMounted) {
|
||||
wasHmdMounted = hmdMounted;
|
||||
goAway();
|
||||
}
|
||||
}
|
||||
|
||||
Script.update.connect(maybeMoveOverlay);
|
||||
|
||||
Script.update.connect(maybeGoAway);
|
||||
Controller.mousePressEvent.connect(goActive);
|
||||
Controller.keyPressEvent.connect(maybeGoActive);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"use strict";
|
||||
// handControllerGrab.js
|
||||
//
|
||||
// Created by Eric Levin on 9/2/15
|
||||
|
@ -33,6 +34,8 @@ var TRIGGER_OFF_VALUE = 0.15;
|
|||
|
||||
var BUMPER_ON_VALUE = 0.5;
|
||||
|
||||
var THUMB_ON_VALUE = 0.5;
|
||||
|
||||
var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move.
|
||||
|
||||
var PICK_WITH_HAND_RAY = true;
|
||||
|
@ -73,9 +76,7 @@ var PICK_MAX_DISTANCE = 500; // max length of pick-ray
|
|||
|
||||
var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected
|
||||
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
|
||||
var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable.
|
||||
var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected
|
||||
var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things
|
||||
var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object
|
||||
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
|
||||
var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size
|
||||
|
@ -122,7 +123,8 @@ var GRABBABLE_PROPERTIES = [
|
|||
"parentID",
|
||||
"parentJointIndex",
|
||||
"density",
|
||||
"dimensions"
|
||||
"dimensions",
|
||||
"userData"
|
||||
];
|
||||
|
||||
var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js
|
||||
|
@ -153,32 +155,39 @@ var USE_POINTLIGHT = false;
|
|||
// states for the state machine
|
||||
var STATE_OFF = 0;
|
||||
var STATE_SEARCHING = 1;
|
||||
var STATE_DISTANCE_HOLDING = 2;
|
||||
var STATE_CONTINUE_DISTANCE_HOLDING = 3;
|
||||
var STATE_NEAR_GRABBING = 4;
|
||||
var STATE_CONTINUE_NEAR_GRABBING = 5;
|
||||
var STATE_NEAR_TRIGGER = 6;
|
||||
var STATE_CONTINUE_NEAR_TRIGGER = 7;
|
||||
var STATE_FAR_TRIGGER = 8;
|
||||
var STATE_CONTINUE_FAR_TRIGGER = 9;
|
||||
var STATE_RELEASE = 10;
|
||||
var STATE_EQUIP_SEARCHING = 11;
|
||||
var STATE_EQUIP = 12
|
||||
var STATE_CONTINUE_EQUIP_BD = 13; // equip while bumper is still held down
|
||||
var STATE_CONTINUE_EQUIP = 14;
|
||||
var STATE_WAITING_FOR_BUMPER_RELEASE = 15;
|
||||
var STATE_HOLD_SEARCHING = 2;
|
||||
var STATE_DISTANCE_HOLDING = 3;
|
||||
var STATE_CONTINUE_DISTANCE_HOLDING = 4;
|
||||
var STATE_NEAR_GRABBING = 5;
|
||||
var STATE_CONTINUE_NEAR_GRABBING = 6;
|
||||
var STATE_NEAR_TRIGGER = 7;
|
||||
var STATE_CONTINUE_NEAR_TRIGGER = 8;
|
||||
var STATE_FAR_TRIGGER = 9;
|
||||
var STATE_CONTINUE_FAR_TRIGGER = 10;
|
||||
var STATE_RELEASE = 11;
|
||||
var STATE_EQUIP = 12;
|
||||
var STATE_HOLD = 13;
|
||||
var STATE_CONTINUE_HOLD = 14;
|
||||
var STATE_CONTINUE_EQUIP = 15;
|
||||
var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 16;
|
||||
var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 17;
|
||||
|
||||
// "collidesWith" is specified by comma-separated list of group names
|
||||
// the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar
|
||||
var COLLIDES_WITH_WHILE_GRABBED = "dynamic,otherAvatar";
|
||||
var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic";
|
||||
|
||||
var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC;
|
||||
var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC;
|
||||
|
||||
function stateToName(state) {
|
||||
switch (state) {
|
||||
case STATE_OFF:
|
||||
return "off";
|
||||
case STATE_SEARCHING:
|
||||
return "searching";
|
||||
case STATE_HOLD_SEARCHING:
|
||||
return "hold_searching";
|
||||
case STATE_DISTANCE_HOLDING:
|
||||
return "distance_holding";
|
||||
case STATE_CONTINUE_DISTANCE_HOLDING:
|
||||
|
@ -197,16 +206,18 @@ function stateToName(state) {
|
|||
return "continue_far_trigger";
|
||||
case STATE_RELEASE:
|
||||
return "release";
|
||||
case STATE_EQUIP_SEARCHING:
|
||||
return "equip_searching";
|
||||
case STATE_EQUIP:
|
||||
return "equip";
|
||||
case STATE_CONTINUE_EQUIP_BD:
|
||||
return "continue_equip_bd";
|
||||
case STATE_HOLD:
|
||||
return "hold";
|
||||
case STATE_CONTINUE_HOLD:
|
||||
return "continue_hold";
|
||||
case STATE_CONTINUE_EQUIP:
|
||||
return "continue_equip";
|
||||
case STATE_WAITING_FOR_BUMPER_RELEASE:
|
||||
return "waiting_for_bumper_release";
|
||||
case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE:
|
||||
return "waiting_for_equip_thumb_release";
|
||||
case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE:
|
||||
return "waiting_for_release_thumb_release";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
|
@ -257,9 +268,13 @@ function MyController(hand) {
|
|||
this.grabbedEntity = null; // on this entity.
|
||||
this.state = STATE_OFF;
|
||||
this.pointer = null; // entity-id of line object
|
||||
this.entityActivated = false;
|
||||
|
||||
this.triggerValue = 0; // rolling average of trigger value
|
||||
this.rawTriggerValue = 0;
|
||||
this.rawBumperValue = 0;
|
||||
this.rawThumbValue = 0;
|
||||
|
||||
//for visualizations
|
||||
this.overlayLine = null;
|
||||
this.particleBeamObject = null;
|
||||
|
@ -293,9 +308,7 @@ function MyController(hand) {
|
|||
this.touchTest();
|
||||
break;
|
||||
case STATE_SEARCHING:
|
||||
this.search();
|
||||
break;
|
||||
case STATE_EQUIP_SEARCHING:
|
||||
case STATE_HOLD_SEARCHING:
|
||||
this.search();
|
||||
break;
|
||||
case STATE_DISTANCE_HOLDING:
|
||||
|
@ -306,13 +319,17 @@ function MyController(hand) {
|
|||
break;
|
||||
case STATE_NEAR_GRABBING:
|
||||
case STATE_EQUIP:
|
||||
case STATE_HOLD:
|
||||
this.nearGrabbing();
|
||||
break;
|
||||
case STATE_WAITING_FOR_BUMPER_RELEASE:
|
||||
this.waitingForBumperRelease();
|
||||
case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE:
|
||||
this.waitingForEquipThumbRelease();
|
||||
break;
|
||||
case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE:
|
||||
this.waitingForReleaseThumbRelease();
|
||||
break;
|
||||
case STATE_CONTINUE_NEAR_GRABBING:
|
||||
case STATE_CONTINUE_EQUIP_BD:
|
||||
case STATE_CONTINUE_HOLD:
|
||||
case STATE_CONTINUE_EQUIP:
|
||||
this.continueNearGrabbing();
|
||||
break;
|
||||
|
@ -564,7 +581,7 @@ function MyController(hand) {
|
|||
"additiveBlending": 0,
|
||||
"textures": "https://hifi-content.s3.amazonaws.com/alan/dev/textures/grabsprite-3.png"
|
||||
}
|
||||
|
||||
|
||||
this.particleBeamObject = Entities.addEntity(particleBeamPropertiesObject);
|
||||
};
|
||||
|
||||
|
@ -782,6 +799,26 @@ function MyController(hand) {
|
|||
return _this.rawBumperValue < BUMPER_ON_VALUE;
|
||||
};
|
||||
|
||||
// this.triggerOrBumperSqueezed = function() {
|
||||
// return triggerSmoothedSqueezed() || bumperSqueezed();
|
||||
// }
|
||||
|
||||
// this.triggerAndBumperReleased = function() {
|
||||
// return triggerSmoothedReleased() && bumperReleased();
|
||||
// }
|
||||
|
||||
this.thumbPress = function(value) {
|
||||
_this.rawThumbValue = value;
|
||||
}
|
||||
|
||||
this.thumbPressed = function() {
|
||||
return _this.rawThumbValue > THUMB_ON_VALUE;
|
||||
};
|
||||
|
||||
this.thumbReleased = function() {
|
||||
return _this.rawThumbValue < THUMB_ON_VALUE;
|
||||
};
|
||||
|
||||
this.off = function() {
|
||||
if (this.triggerSmoothedSqueezed() || this.bumperSqueezed()) {
|
||||
this.lastPickTime = 0;
|
||||
|
@ -789,8 +826,8 @@ function MyController(hand) {
|
|||
this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation;
|
||||
if (this.triggerSmoothedSqueezed()) {
|
||||
this.setState(STATE_SEARCHING);
|
||||
} else {
|
||||
this.setState(STATE_EQUIP_SEARCHING);
|
||||
} else if (this.bumperSqueezed()) {
|
||||
this.setState(STATE_HOLD_SEARCHING);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -800,7 +837,13 @@ function MyController(hand) {
|
|||
this.isInitialGrab = false;
|
||||
this.doubleParentGrab = false;
|
||||
|
||||
if (this.state == STATE_SEARCHING ? this.triggerSmoothedReleased() : this.bumperReleased()) {
|
||||
this.checkForStrayChildren();
|
||||
|
||||
if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_HOLD_SEARCHING && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
return;
|
||||
}
|
||||
|
@ -814,18 +857,23 @@ function MyController(hand) {
|
|||
|
||||
var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
||||
var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation;
|
||||
var currentControllerPosition = Controller.getPoseValue(controllerHandInput).position;
|
||||
var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation));
|
||||
|
||||
var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ?
|
||||
Controller.Standard.RightHand : Controller.Standard.LeftHand);
|
||||
var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
|
||||
|
||||
var distantPickRay = {
|
||||
origin: PICK_WITH_HAND_RAY ? handPosition : Camera.position,
|
||||
direction: PICK_WITH_HAND_RAY ? Quat.getUp(this.getHandRotation()) : Vec3.mix(Quat.getUp(this.getHandRotation()),
|
||||
direction: PICK_WITH_HAND_RAY ? Quat.getUp(controllerRotation) : Vec3.mix(Quat.getUp(controllerRotation),
|
||||
Quat.getFront(Camera.orientation),
|
||||
HAND_HEAD_MIX_RATIO),
|
||||
length: PICK_MAX_DISTANCE
|
||||
};
|
||||
|
||||
var searchVisualizationPickRay = {
|
||||
origin: handPosition,
|
||||
origin: currentControllerPosition,
|
||||
direction: Quat.getUp(this.getHandRotation()),
|
||||
length: PICK_MAX_DISTANCE
|
||||
};
|
||||
|
@ -932,7 +980,7 @@ function MyController(hand) {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
if (propsForCandidate.parentID != NULL_UUID && this.state == STATE_EQUIP_SEARCHING) {
|
||||
if (propsForCandidate.parentID != NULL_UUID && this.state == STATE_HOLD_SEARCHING) {
|
||||
// don't allow a double-equip
|
||||
if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) {
|
||||
print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': it's a child");
|
||||
|
@ -966,15 +1014,19 @@ function MyController(hand) {
|
|||
this.setState(near ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER);
|
||||
return;
|
||||
}
|
||||
// near grab or equip with action
|
||||
// near grab with action or equip
|
||||
var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {});
|
||||
var refCount = ("refCount" in grabData) ? grabData.refCount : 0;
|
||||
if (near && (refCount < 1 || entityHasActions(this.grabbedEntity))) {
|
||||
this.setState(this.state == STATE_SEARCHING ? STATE_NEAR_GRABBING : STATE_EQUIP);
|
||||
if (this.state == STATE_SEARCHING) {
|
||||
this.setState(STATE_NEAR_GRABBING);
|
||||
} else { // (this.state == STATE_HOLD_SEARCHING)
|
||||
this.setState(STATE_HOLD);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// far grab or equip with action
|
||||
if ((isPhysical || this.state == STATE_EQUIP_SEARCHING) && !near) {
|
||||
// far grab
|
||||
if (isPhysical && !near) {
|
||||
if (entityIsGrabbedByOther(this.grabbedEntity)) {
|
||||
// don't distance grab something that is already grabbed.
|
||||
if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) {
|
||||
|
@ -995,15 +1047,20 @@ function MyController(hand) {
|
|||
intersectionPointToCenterDistance *
|
||||
FAR_TO_NEAR_GRAB_PADDING_FACTOR);
|
||||
}
|
||||
this.setState(this.state == STATE_SEARCHING ? STATE_DISTANCE_HOLDING : STATE_EQUIP);
|
||||
this.setState(STATE_DISTANCE_HOLDING);
|
||||
|
||||
this.searchSphereOff();
|
||||
return;
|
||||
}
|
||||
|
||||
// else this thing isn't physical. grab it by reparenting it (but not if we've already
|
||||
// grabbed it).
|
||||
if (grabbableData.refCount < 1) {
|
||||
this.setState(this.state == STATE_SEARCHING ? STATE_NEAR_GRABBING : STATE_EQUIP);
|
||||
if (refCount < 1) {
|
||||
if (this.state == STATE_SEARCHING) {
|
||||
this.setState(STATE_NEAR_GRABBING);
|
||||
} else { // this.state == STATE_HOLD_SEARCHING)
|
||||
this.setState(STATE_HOLD);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// it's not physical and it's already held via parenting. go ahead and grab it, but
|
||||
|
@ -1012,7 +1069,11 @@ function MyController(hand) {
|
|||
this.doubleParentGrab = true;
|
||||
this.previousParentID = props.parentID;
|
||||
this.previousParentJointIndex = props.parentJointIndex;
|
||||
this.setState(this.state == STATE_SEARCHING ? STATE_NEAR_GRABBING : STATE_EQUIP);
|
||||
if (this.state == STATE_SEARCHING) {
|
||||
this.setState(STATE_NEAR_GRABBING);
|
||||
} else { // (this.state == STATE_HOLD_SEARCHING)
|
||||
this.setState(STATE_HOLD);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) {
|
||||
|
@ -1045,9 +1106,16 @@ 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();
|
||||
|
||||
|
@ -1055,12 +1123,13 @@ 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;
|
||||
|
||||
this.grabRadius = Vec3.distance(this.currentObjectPosition, controllerPosition);
|
||||
this.grabRadialVelocity = 0.0;
|
||||
|
||||
// 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(this.grabRadius + 1.0);
|
||||
if (this.radiusScalar < 1.0) {
|
||||
this.radiusScalar = 1.0;
|
||||
}
|
||||
|
@ -1090,96 +1159,55 @@ 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() {
|
||||
if (this.triggerSmoothedReleased()) {
|
||||
if (this.triggerSmoothedReleased() && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("releaseGrab");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
this.heartBeat(this.grabbedEntity);
|
||||
|
||||
// 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);
|
||||
|
||||
if (this.state == STATE_CONTINUE_DISTANCE_HOLDING && this.bumperSqueezed() &&
|
||||
this.hasPresetOffsets()) {
|
||||
var saveGrabbedID = this.grabbedEntity;
|
||||
this.release();
|
||||
this.setState(STATE_EQUIP);
|
||||
this.grabbedEntity = saveGrabbedID;
|
||||
return;
|
||||
}
|
||||
var now = Date.now();
|
||||
var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds
|
||||
this.currentObjectTime = now;
|
||||
|
||||
|
||||
// the action was set up on a previous call. update the targets.
|
||||
var radius = Vec3.distance(this.currentObjectPosition, handControllerPosition) *
|
||||
// the action was set up when this.distanceHolding was called. update the targets.
|
||||
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;
|
||||
this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
|
||||
// update the currentObject position and rotation.
|
||||
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
|
||||
// this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
|
||||
|
||||
this.callEntityMethodOnGrabbed("continueDistantGrab");
|
||||
|
||||
|
@ -1189,6 +1217,26 @@ function MyController(hand) {
|
|||
|
||||
var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData);
|
||||
|
||||
// Update radialVelocity
|
||||
var lastVelocity = Vec3.subtract(controllerPosition, this.previousControllerPosition);
|
||||
lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime);
|
||||
var newRadialVelocity = Vec3.dot(lastVelocity,
|
||||
Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition)));
|
||||
|
||||
var VELOCITY_AVERAGING_TIME = 0.016;
|
||||
this.grabRadialVelocity = (deltaTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity +
|
||||
(1.0 - (deltaTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity;
|
||||
|
||||
var RADIAL_GRAB_AMPLIFIER = 10.0;
|
||||
if (Math.abs(this.grabRadialVelocity) > 0.0) {
|
||||
this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER);
|
||||
}
|
||||
|
||||
var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation));
|
||||
newTargetPosition = Vec3.sum(newTargetPosition, controllerPosition);
|
||||
|
||||
|
||||
var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position);
|
||||
if (handControllerData.disableMoveWithHead !== true) {
|
||||
// mix in head motion
|
||||
if (MOVE_WITH_HEAD) {
|
||||
|
@ -1228,6 +1276,7 @@ function MyController(hand) {
|
|||
}
|
||||
}
|
||||
|
||||
var handPosition = this.getHandPosition();
|
||||
|
||||
//visualizations
|
||||
if (USE_ENTITY_LINES_FOR_MOVING === true) {
|
||||
|
@ -1248,7 +1297,7 @@ function MyController(hand) {
|
|||
|
||||
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
|
||||
var success = Entities.updateAction(this.grabbedEntity, this.actionID, {
|
||||
targetPosition: targetPosition,
|
||||
targetPosition: newTargetPosition,
|
||||
linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
|
||||
targetRotation: this.currentObjectRotation,
|
||||
angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
|
||||
|
@ -1259,6 +1308,9 @@ function MyController(hand) {
|
|||
} else {
|
||||
print("continueDistanceHolding -- updateAction failed");
|
||||
}
|
||||
|
||||
this.previousControllerPosition = controllerPosition;
|
||||
this.previousControllerRotation = controllerRotation;
|
||||
};
|
||||
|
||||
this.setupHoldAction = function() {
|
||||
|
@ -1329,33 +1381,37 @@ function MyController(hand) {
|
|||
|
||||
this.nearGrabbing = function() {
|
||||
var now = Date.now();
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
|
||||
if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("releaseGrab");
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_HOLD && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("releaseGrab");
|
||||
return;
|
||||
}
|
||||
|
||||
this.lineOff();
|
||||
this.overlayLineOff();
|
||||
|
||||
if (this.entityActivated) {
|
||||
var saveGrabbedID = this.grabbedEntity;
|
||||
this.release();
|
||||
this.grabbedEntity = saveGrabbedID;
|
||||
}
|
||||
|
||||
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
|
||||
this.activateEntity(this.grabbedEntity, grabbedProperties, false);
|
||||
if (grabbedProperties.dynamic && NEAR_GRABBING_KINEMATIC) {
|
||||
Entities.editEntity(this.grabbedEntity, {
|
||||
dynamic: false
|
||||
});
|
||||
}
|
||||
|
||||
// var handRotation = this.getHandRotation();
|
||||
var handRotation = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation();
|
||||
var handPosition = this.getHandPosition();
|
||||
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
|
||||
var hasPresetPosition = false;
|
||||
if (this.state != STATE_NEAR_GRABBING && this.hasPresetOffsets()) {
|
||||
if ((this.state == STATE_EQUIP || this.state == STATE_HOLD) && this.hasPresetOffsets()) {
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
// if an object is "equipped" and has a predefined offset, use it.
|
||||
this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false;
|
||||
this.offsetPosition = this.getPresetPosition();
|
||||
|
@ -1370,9 +1426,9 @@ function MyController(hand) {
|
|||
var currentObjectPosition = grabbedProperties.position;
|
||||
var offset = Vec3.subtract(currentObjectPosition, handPosition);
|
||||
this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset);
|
||||
if (this.temporaryPositionOffset && this.state != STATE_NEAR_GRABBING) {
|
||||
if (this.temporaryPositionOffset && (this.state == STATE_EQUIP)) {
|
||||
this.offsetPosition = this.temporaryPositionOffset;
|
||||
hasPresetPosition = true;
|
||||
// hasPresetPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1395,25 +1451,44 @@ function MyController(hand) {
|
|||
reparentProps["localRotation"] = this.offsetRotation;
|
||||
}
|
||||
Entities.editEntity(this.grabbedEntity, reparentProps);
|
||||
|
||||
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
||||
action: 'equip',
|
||||
grabbedEntity: this.grabbedEntity
|
||||
}));
|
||||
}
|
||||
|
||||
this.callEntityMethodOnGrabbed(this.state == STATE_NEAR_GRABBING ? "startNearGrab" : "startEquip");
|
||||
Entities.editEntity(this.grabbedEntity, {
|
||||
velocity: {x: 0, y: 0, z: 0},
|
||||
angularVelocity: {x: 0, y: 0, z: 0},
|
||||
dynamic: false
|
||||
});
|
||||
|
||||
if (this.state == STATE_NEAR_GRABBING) {
|
||||
this.callEntityMethodOnGrabbed("startNearGrab");
|
||||
} else { // this.state == STATE_EQUIP || this.state == STATE_HOLD
|
||||
this.callEntityMethodOnGrabbed("startEquip");
|
||||
}
|
||||
|
||||
if (this.state == STATE_NEAR_GRABBING) {
|
||||
// near grabbing
|
||||
this.setState(STATE_CONTINUE_NEAR_GRABBING);
|
||||
} else {
|
||||
} else if (this.state == STATE_HOLD) {
|
||||
// holding
|
||||
this.setState(STATE_CONTINUE_HOLD);
|
||||
} else { // (this.state == STATE_EQUIP)
|
||||
// equipping
|
||||
this.setState(STATE_CONTINUE_EQUIP_BD);
|
||||
this.setState(STATE_CONTINUE_EQUIP);
|
||||
}
|
||||
|
||||
this.currentHandControllerTipPosition =
|
||||
(this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition;
|
||||
this.currentObjectTime = Date.now();
|
||||
|
||||
this.currentObjectPosition = grabbedProperties.position;
|
||||
this.currentObjectRotation = grabbedProperties.rotation;
|
||||
this.currentVelocity = ZERO_VEC;
|
||||
this.currentAngularVelocity = ZERO_VEC;
|
||||
};
|
||||
|
||||
this.continueNearGrabbing = function() {
|
||||
|
@ -1422,30 +1497,48 @@ function MyController(hand) {
|
|||
this.callEntityMethodOnGrabbed("releaseGrab");
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_CONTINUE_EQUIP_BD && this.bumperReleased()) {
|
||||
this.setState(STATE_CONTINUE_EQUIP);
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_CONTINUE_EQUIP && this.bumperSqueezed()) {
|
||||
this.setState(STATE_WAITING_FOR_BUMPER_RELEASE);
|
||||
if (this.state == STATE_CONTINUE_HOLD && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("releaseEquip");
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.bumperSqueezed()) {
|
||||
this.setState(STATE_CONTINUE_EQUIP_BD);
|
||||
if (this.state == STATE_CONTINUE_EQUIP && this.thumbPressed()) {
|
||||
this.setState(STATE_WAITING_FOR_RELEASE_THUMB_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("releaseEquip");
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.thumbPressed()) {
|
||||
this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("releaseGrab");
|
||||
this.callEntityMethodOnGrabbed("startEquip");
|
||||
return;
|
||||
}
|
||||
if (this.state == STATE_CONTINUE_HOLD && this.thumbPressed()) {
|
||||
this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE);
|
||||
return;
|
||||
}
|
||||
|
||||
this.heartBeat(this.grabbedEntity);
|
||||
|
||||
var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]);
|
||||
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;
|
||||
}
|
||||
|
||||
var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position"]);
|
||||
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.
|
||||
print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." +
|
||||
props.parentID + " " + vec3toStr(props.position));
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed(this.state == STATE_NEAR_GRABBING ? "releaseGrab" : "releaseEquip");
|
||||
if (this.state == STATE_CONTINUE_NEAR_GRABBING) {
|
||||
this.callEntityMethodOnGrabbed("releaseGrab");
|
||||
} else { // (this.state == STATE_CONTINUE_EQUIP || this.state == STATE_CONTINUE_HOLD)
|
||||
this.callEntityMethodOnGrabbed("releaseEquip");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1462,11 +1555,25 @@ function MyController(hand) {
|
|||
var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerTipPosition); // meters
|
||||
var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds
|
||||
|
||||
if (deltaTime > 0.0) {
|
||||
var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition);
|
||||
|
||||
var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation);
|
||||
var newEulers = Quat.safeEulerAngles(props.rotation);
|
||||
var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers);
|
||||
|
||||
this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaTime);
|
||||
this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaTime * 180.0));
|
||||
|
||||
this.currentObjectPosition = props.position;
|
||||
this.currentObjectRotation = props.rotation;
|
||||
}
|
||||
|
||||
this.currentHandControllerTipPosition = handControllerPosition;
|
||||
this.currentObjectTime = now;
|
||||
|
||||
var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {});
|
||||
if (this.state === STATE_CONTINUE_EQUIP) {
|
||||
if (this.state === STATE_CONTINUE_EQUIP || this.state === STATE_CONTINUE_HOLD) {
|
||||
this.callEntityMethodOnGrabbed("continueEquip");
|
||||
}
|
||||
if (this.state == STATE_CONTINUE_NEAR_GRABBING) {
|
||||
|
@ -1495,14 +1602,19 @@ function MyController(hand) {
|
|||
}
|
||||
};
|
||||
|
||||
this.waitingForBumperRelease = function() {
|
||||
if (this.bumperReleased()) {
|
||||
this.waitingForEquipThumbRelease = function() {
|
||||
if (this.thumbReleased() && this.triggerSmoothedReleased()) {
|
||||
this.setState(STATE_EQUIP);
|
||||
}
|
||||
};
|
||||
this.waitingForReleaseThumbRelease = function() {
|
||||
if (this.thumbReleased() && this.triggerSmoothedReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
}
|
||||
};
|
||||
|
||||
this.nearTrigger = function() {
|
||||
if (this.triggerSmoothedReleased()) {
|
||||
if (this.triggerSmoothedReleased() && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("stopNearTrigger");
|
||||
return;
|
||||
|
@ -1512,7 +1624,7 @@ function MyController(hand) {
|
|||
};
|
||||
|
||||
this.farTrigger = function() {
|
||||
if (this.triggerSmoothedReleased()) {
|
||||
if (this.triggerSmoothedReleased() && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("stopFarTrigger");
|
||||
return;
|
||||
|
@ -1522,7 +1634,7 @@ function MyController(hand) {
|
|||
};
|
||||
|
||||
this.continueNearTrigger = function() {
|
||||
if (this.triggerSmoothedReleased()) {
|
||||
if (this.triggerSmoothedReleased() && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("stopNearTrigger");
|
||||
return;
|
||||
|
@ -1531,7 +1643,7 @@ function MyController(hand) {
|
|||
};
|
||||
|
||||
this.continueFarTrigger = function() {
|
||||
if (this.triggerSmoothedReleased()) {
|
||||
if (this.triggerSmoothedReleased() && this.bumperReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
this.callEntityMethodOnGrabbed("stopFarTrigger");
|
||||
return;
|
||||
|
@ -1587,52 +1699,48 @@ function MyController(hand) {
|
|||
}
|
||||
|
||||
ids.forEach(function(id) {
|
||||
|
||||
var props = Entities.getEntityProperties(id, ["boundingBox", "name"]);
|
||||
if (props.name === 'pointer') {
|
||||
if (!props ||
|
||||
!props.boundingBox ||
|
||||
props.name === 'pointer') {
|
||||
return;
|
||||
} else {
|
||||
var entityMinPoint = props.boundingBox.brn;
|
||||
var entityMaxPoint = props.boundingBox.tfl;
|
||||
var leftIsTouching = pointInExtents(leftHandPosition, entityMinPoint, entityMaxPoint);
|
||||
var rightIsTouching = pointInExtents(rightHandPosition, entityMinPoint, entityMaxPoint);
|
||||
|
||||
if ((leftIsTouching || rightIsTouching) && _this.allTouchedIDs[id] === undefined) {
|
||||
// we haven't been touched before, but either right or left is touching us now
|
||||
_this.allTouchedIDs[id] = true;
|
||||
_this.startTouch(id);
|
||||
} else if ((leftIsTouching || rightIsTouching) && _this.allTouchedIDs[id]) {
|
||||
// we have been touched before and are still being touched
|
||||
// continue touch
|
||||
_this.continueTouch(id);
|
||||
} else if (_this.allTouchedIDs[id]) {
|
||||
delete _this.allTouchedIDs[id];
|
||||
_this.stopTouch(id);
|
||||
|
||||
} else {
|
||||
//we are in another state
|
||||
return;
|
||||
}
|
||||
}
|
||||
var entityMinPoint = props.boundingBox.brn;
|
||||
var entityMaxPoint = props.boundingBox.tfl;
|
||||
var leftIsTouching = pointInExtents(leftHandPosition, entityMinPoint, entityMaxPoint);
|
||||
var rightIsTouching = pointInExtents(rightHandPosition, entityMinPoint, entityMaxPoint);
|
||||
|
||||
if ((leftIsTouching || rightIsTouching) && _this.allTouchedIDs[id] === undefined) {
|
||||
// we haven't been touched before, but either right or left is touching us now
|
||||
_this.allTouchedIDs[id] = true;
|
||||
_this.startTouch(id);
|
||||
} else if ((leftIsTouching || rightIsTouching) && _this.allTouchedIDs[id]) {
|
||||
// we have been touched before and are still being touched
|
||||
// continue touch
|
||||
_this.continueTouch(id);
|
||||
} else if (_this.allTouchedIDs[id]) {
|
||||
delete _this.allTouchedIDs[id];
|
||||
_this.stopTouch(id);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
this.startTouch = function(entityID) {
|
||||
this.callEntityMethodOnGrabbed("startTouch");
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(entityID, "startTouch", args);
|
||||
};
|
||||
|
||||
this.continueTouch = function(entityID) {
|
||||
this.callEntityMethodOnGrabbed("continueTouch");
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(entityID, "continueTouch", args);
|
||||
};
|
||||
|
||||
this.stopTouch = function(entityID) {
|
||||
this.callEntityMethodOnGrabbed("stopTouch");
|
||||
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
||||
Entities.callEntityMethod(entityID, "stopTouch", args);
|
||||
};
|
||||
|
||||
this.release = function() {
|
||||
|
||||
this.turnLightsOff();
|
||||
this.turnOffVisualizations();
|
||||
|
||||
|
@ -1647,7 +1755,7 @@ function MyController(hand) {
|
|||
// this next line allowed both:
|
||||
// (1) far-grab, pull to self, near grab, then throw
|
||||
// (2) equip something physical and adjust it with a other-hand grab without the thing drifting
|
||||
(!this.isInitialGrab && grabData.refCount > 1)) {
|
||||
grabData.refCount > 1) {
|
||||
noVelocity = true;
|
||||
}
|
||||
}
|
||||
|
@ -1673,19 +1781,43 @@ function MyController(hand) {
|
|||
Entities.deleteEntity(this.pointLight);
|
||||
};
|
||||
|
||||
this.activateEntity = function(entityID, grabbedProperties, wasLoaded) {
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA);
|
||||
this.heartBeat = function(entityID) {
|
||||
var now = Date.now();
|
||||
if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) {
|
||||
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
||||
data["heartBeat"] = now;
|
||||
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
|
||||
this.lastHeartBeat = now;
|
||||
}
|
||||
};
|
||||
|
||||
this.resetAbandonedGrab = function(entityID) {
|
||||
print("cleaning up abandoned grab on " + entityID);
|
||||
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
||||
data["activated"] = true;
|
||||
data["avatarId"] = MyAvatar.sessionUUID;
|
||||
data["refCount"] = 1;
|
||||
setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data);
|
||||
this.deactivateEntity(entityID, false);
|
||||
};
|
||||
|
||||
this.activateEntity = function(entityID, grabbedProperties, wasLoaded) {
|
||||
if (this.entityActivated) {
|
||||
return;
|
||||
}
|
||||
this.entityActivated = true;
|
||||
|
||||
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
||||
var now = Date.now();
|
||||
|
||||
if (wasLoaded) {
|
||||
data["refCount"] = 1;
|
||||
data["avatarId"] = MyAvatar.sessionUUID;
|
||||
} else {
|
||||
data["refCount"] = data["refCount"] ? data["refCount"] + 1 : 1;
|
||||
|
||||
// zero gravity and set ignoreForCollisions in a way that lets us put them back, after all grabs are done
|
||||
if (data["refCount"] == 1) {
|
||||
data["heartBeat"] = now;
|
||||
this.lastHeartBeat = now;
|
||||
|
||||
this.isInitialGrab = true;
|
||||
data["gravity"] = grabbedProperties.gravity;
|
||||
data["collidesWith"] = grabbedProperties.collidesWith;
|
||||
|
@ -1701,12 +1833,21 @@ function MyController(hand) {
|
|||
z: 0
|
||||
},
|
||||
// bummer, it isn't easy to do bitwise collisionMask operations like this:
|
||||
//"collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask
|
||||
// "collisionMask": COLLISION_MASK_WHILE_GRABBED | grabbedProperties.collisionMask
|
||||
// when using string values
|
||||
"collidesWith": COLLIDES_WITH_WHILE_GRABBED
|
||||
};
|
||||
Entities.editEntity(entityID, whileHeldProperties);
|
||||
} else if (data["refCount"] > 1) {
|
||||
if (data["heartBeat"] === undefined ||
|
||||
now - data["heartBeat"] > HEART_BEAT_TIMEOUT) {
|
||||
// this entity has userData suggesting it is grabbed, but nobody is updating the hearbeat.
|
||||
// deactivate it before grabbing.
|
||||
this.resetAbandonedGrab(entityID);
|
||||
grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
|
||||
return this.activateEntity(entityID, grabbedProperties, wasLoaded);
|
||||
}
|
||||
|
||||
this.isInitialGrab = false;
|
||||
// if an object is being grabbed by more than one person (or the same person twice, but nevermind), switch
|
||||
// the collision groups so that it wont collide with "other" avatars. This avoids a situation where two
|
||||
|
@ -1720,7 +1861,23 @@ 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) {
|
||||
if (!this.entityActivated) {
|
||||
return;
|
||||
}
|
||||
this.entityActivated = false;
|
||||
|
||||
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
||||
if (data && data["refCount"]) {
|
||||
data["refCount"] = data["refCount"] - 1;
|
||||
|
@ -1736,22 +1893,48 @@ function MyController(hand) {
|
|||
|
||||
// things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If
|
||||
// it looks like the dropped thing should fall, give it a little velocity.
|
||||
var parentID = Entities.getEntityProperties(entityID, ["parentID"]).parentID;
|
||||
var props = Entities.getEntityProperties(entityID, ["parentID", "velocity"])
|
||||
var parentID = props.parentID;
|
||||
var forceVelocity = false;
|
||||
|
||||
var doSetVelocity = false;
|
||||
if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID) {
|
||||
// TODO: EntityScriptingInterface::convertLocationToScriptSemantics should be setting up
|
||||
// props.velocity to be a world-frame velocity and localVelocity to be vs parent. Until that
|
||||
// is done, we use a measured velocity here so that things held via a bumper-grab / parenting-grab
|
||||
// can be thrown.
|
||||
doSetVelocity = true;
|
||||
}
|
||||
|
||||
if (!noVelocity &&
|
||||
!doSetVelocity &&
|
||||
parentID == MyAvatar.sessionUUID &&
|
||||
Vec3.length(data["gravity"]) > 0.0 &&
|
||||
data["dynamic"] &&
|
||||
data["parentID"] == NULL_UUID &&
|
||||
!data["collisionless"]) {
|
||||
deactiveProps["velocity"] = {x: 0.0, y: 0.1, z: 0.0};
|
||||
doSetVelocity = false;
|
||||
}
|
||||
if (noVelocity) {
|
||||
deactiveProps["velocity"] = {x: 0.0, y: 0.0, z: 0.0};
|
||||
deactiveProps["angularVelocity"] = {x: 0.0, y: 0.0, z: 0.0};
|
||||
doSetVelocity = false;
|
||||
}
|
||||
|
||||
Entities.editEntity(entityID, deactiveProps);
|
||||
|
||||
if (doSetVelocity) {
|
||||
// this is a continuation of the TODO above -- we shouldn't need to set this here.
|
||||
// do this after the parent has been reset. setting this at the same time as
|
||||
// the parent causes it to go off in the wrong direction. This is a bug that should
|
||||
// be fixed.
|
||||
Entities.editEntity(entityID, {
|
||||
velocity: this.currentVelocity,
|
||||
// angularVelocity: this.currentAngularVelocity
|
||||
});
|
||||
}
|
||||
|
||||
data = null;
|
||||
} else if (this.doubleParentGrab) {
|
||||
// we parent-grabbed this from another parent grab. try to put it back where we found it.
|
||||
|
@ -1775,7 +1958,7 @@ function MyController(hand) {
|
|||
this.checkNewlyLoaded = function(loadedEntityID) {
|
||||
if (this.state == STATE_OFF ||
|
||||
this.state == STATE_SEARCHING ||
|
||||
this.state == STATE_EQUIP_SEARCHING) {
|
||||
this.state == STATE_HOLD_SEARCHING) {
|
||||
var loadedProps = Entities.getEntityProperties(loadedEntityID);
|
||||
if (loadedProps.parentID != MyAvatar.sessionUUID) {
|
||||
return;
|
||||
|
@ -1807,6 +1990,9 @@ mapping.from([Controller.Standard.LT]).peek().to(leftController.triggerPress);
|
|||
mapping.from([Controller.Standard.RB]).peek().to(rightController.bumperPress);
|
||||
mapping.from([Controller.Standard.LB]).peek().to(leftController.bumperPress);
|
||||
|
||||
mapping.from([Controller.Standard.LeftPrimaryThumb]).peek().to(leftController.thumbPress);
|
||||
mapping.from([Controller.Standard.RightPrimaryThumb]).peek().to(rightController.thumbPress);
|
||||
|
||||
Controller.enableMapping(MAPPING_NAME);
|
||||
|
||||
//the section below allows the grab script to listen for messages that disable either one or both hands. useful for two handed items
|
||||
|
|
131
examples/controllers/handControllerMouse.js
Normal file
131
examples/controllers/handControllerMouse.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// handControllerMouse.js
|
||||
// examples/controllers
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2015/12/15
|
||||
// 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
|
||||
//
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
function length(posA, posB) {
|
||||
var dx = posA.x - posB.x;
|
||||
var dy = posA.y - posB.y;
|
||||
var length = Math.sqrt((dx*dx) + (dy*dy))
|
||||
return length;
|
||||
}
|
||||
|
||||
function moveReticleAbsolute(x, y) {
|
||||
var globalPos = Reticle.getPosition();
|
||||
globalPos.x = x;
|
||||
globalPos.y = y;
|
||||
Reticle.setPosition(globalPos);
|
||||
}
|
||||
|
||||
var MAPPING_NAME = "com.highfidelity.testing.reticleWithHandRotation";
|
||||
var mapping = Controller.newMapping(MAPPING_NAME);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
var leftRightBias = 0.0;
|
||||
var filteredRotatedLeft = Vec3.UNIT_NEG_Y;
|
||||
var filteredRotatedRight = Vec3.UNIT_NEG_Y;
|
||||
var lastAlpha = 0;
|
||||
|
||||
Script.update.connect(function(deltaTime) {
|
||||
|
||||
// avatar frame
|
||||
var poseRight = Controller.getPoseValue(Controller.Standard.RightHand);
|
||||
var poseLeft = Controller.getPoseValue(Controller.Standard.LeftHand);
|
||||
|
||||
// NOTE: hack for now
|
||||
var screenSize = Reticle.maximumPosition;
|
||||
var screenSizeX = screenSize.x;
|
||||
var screenSizeY = screenSize.y;
|
||||
|
||||
// transform hand facing vectors from avatar frame into sensor frame.
|
||||
var worldToSensorMatrix = Mat4.inverse(MyAvatar.sensorToWorldMatrix);
|
||||
var rotatedRight = Mat4.transformVector(worldToSensorMatrix, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.multiplyQbyV(poseRight.rotation, Vec3.UNIT_NEG_Y)));
|
||||
var rotatedLeft = Mat4.transformVector(worldToSensorMatrix, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.multiplyQbyV(poseLeft.rotation, Vec3.UNIT_NEG_Y)));
|
||||
|
||||
lastRotatedRight = rotatedRight;
|
||||
|
||||
// Decide which hand should be controlling the pointer
|
||||
// by comparing which one is moving more, and by
|
||||
// tending to stay with the one moving more.
|
||||
if (deltaTime > 0.001) {
|
||||
// leftRightBias is a running average of the difference in angular hand speed.
|
||||
// a positive leftRightBias indicates the right hand is spinning faster then the left hand.
|
||||
// a negative leftRightBias indicates the left hand is spnning faster.
|
||||
var BIAS_ADJUST_PERIOD = 1.0;
|
||||
var tau = Math.clamp(deltaTime / BIAS_ADJUST_PERIOD, 0, 1);
|
||||
newLeftRightBias = Vec3.length(poseRight.angularVelocity) - Vec3.length(poseLeft.angularVelocity);
|
||||
leftRightBias = (1 - tau) * leftRightBias + tau * newLeftRightBias;
|
||||
}
|
||||
|
||||
// add a bit of hysteresis to prevent control flopping back and forth
|
||||
// between hands when they are both mostly stationary.
|
||||
var alpha;
|
||||
var HYSTERESIS_OFFSET = 0.25;
|
||||
if (lastAlpha > 0.5) {
|
||||
// prefer right hand over left
|
||||
alpha = leftRightBias > -HYSTERESIS_OFFSET ? 1 : 0;
|
||||
} else {
|
||||
alpha = leftRightBias > HYSTERESIS_OFFSET ? 1 : 0;
|
||||
}
|
||||
lastAlpha = alpha;
|
||||
|
||||
// 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 = 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, alpha);
|
||||
|
||||
var absolutePitch = rotated.y; // from 1 down to -1 up ... but note: if you rotate down "too far" it starts to go up again...
|
||||
var absoluteYaw = -rotated.x; // from -1 left to 1 right
|
||||
|
||||
var x = Math.clamp(screenSizeX * (absoluteYaw + 0.5), 0, screenSizeX);
|
||||
var y = Math.clamp(screenSizeX * absolutePitch, 0, screenSizeY);
|
||||
|
||||
// don't move the reticle with the hand controllers unless the controllers are actually being moved
|
||||
// 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 - alpha) + Vec3.length(poseRight.angularVelocity) * alpha;
|
||||
angularVelocityTrailingAverage = angularVelocityTrailingAverage * AVERAGING_INTERVAL + angularVelocityMagnitude * (1.0 - AVERAGING_INTERVAL);
|
||||
|
||||
if ((angularVelocityTrailingAverage > MINIMUM_CONTROLLER_ANGULAR_VELOCITY) && ((x != lastX) || (y != lastY))) {
|
||||
moveReticleAbsolute(x, y);
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
});
|
||||
|
||||
Script.scriptEnding.connect(function(){
|
||||
mapping.disable();
|
||||
});
|
|
@ -11,6 +11,21 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var leftTriggerValue = 0;
|
||||
var rightTriggerValue = 0;
|
||||
|
||||
var LEAP_TRIGGER_START_ANGLE = 15.0;
|
||||
var LEAP_TRIGGER_END_ANGLE = 40.0;
|
||||
|
||||
function getLeapMotionLeftTrigger() {
|
||||
//print("left trigger = " + leftTriggerValue);
|
||||
return leftTriggerValue;
|
||||
}
|
||||
function getLeapMotionRightTrigger() {
|
||||
//print("right trigger = " + rightTriggerValue);
|
||||
return rightTriggerValue;
|
||||
}
|
||||
|
||||
var leapHands = (function () {
|
||||
|
||||
var isOnHMD,
|
||||
|
@ -61,78 +76,6 @@ var leapHands = (function () {
|
|||
print(i + ": " + jointNames[i]);
|
||||
}
|
||||
print("... skeleton joint names");
|
||||
|
||||
/*
|
||||
http://public.highfidelity.io/models/skeletons/ron_standing.fst
|
||||
Skeleton joint names ...
|
||||
0: Hips
|
||||
1: RightUpLeg
|
||||
2: RightLeg
|
||||
3: RightFoot
|
||||
4: RightToeBase
|
||||
5: RightToe_End
|
||||
6: LeftUpLeg
|
||||
7: LeftLeg
|
||||
8: LeftFoot
|
||||
9: LeftToeBase
|
||||
10: LeftToe_End
|
||||
11: Spine
|
||||
12: Spine1
|
||||
13: Spine2
|
||||
14: RightShoulder
|
||||
15: RightArm
|
||||
16: RightForeArm
|
||||
17: RightHand
|
||||
18: RightHandPinky1
|
||||
19: RightHandPinky2
|
||||
20: RightHandPinky3
|
||||
21: RightHandPinky4
|
||||
22: RightHandRing1
|
||||
23: RightHandRing2
|
||||
24: RightHandRing3
|
||||
25: RightHandRing4
|
||||
26: RightHandMiddle1
|
||||
27: RightHandMiddle2
|
||||
28: RightHandMiddle3
|
||||
29: RightHandMiddle4
|
||||
30: RightHandIndex1
|
||||
31: RightHandIndex2
|
||||
32: RightHandIndex3
|
||||
33: RightHandIndex4
|
||||
34: RightHandThumb1
|
||||
35: RightHandThumb2
|
||||
36: RightHandThumb3
|
||||
37: RightHandThumb4
|
||||
38: LeftShoulder
|
||||
39: LeftArm
|
||||
40: LeftForeArm
|
||||
41: LeftHand
|
||||
42: LeftHandPinky1
|
||||
43: LeftHandPinky2
|
||||
44: LeftHandPinky3
|
||||
45: LeftHandPinky4
|
||||
46: LeftHandRing1
|
||||
47: LeftHandRing2
|
||||
48: LeftHandRing3
|
||||
49: LeftHandRing4
|
||||
50: LeftHandMiddle1
|
||||
51: LeftHandMiddle2
|
||||
52: LeftHandMiddle3
|
||||
53: LeftHandMiddle4
|
||||
54: LeftHandIndex1
|
||||
55: LeftHandIndex2
|
||||
56: LeftHandIndex3
|
||||
57: LeftHandIndex4
|
||||
58: LeftHandThumb1
|
||||
59: LeftHandThumb2
|
||||
60: LeftHandThumb3
|
||||
61: LeftHandThumb4
|
||||
62: Neck
|
||||
63: Head
|
||||
64: HeadTop_End
|
||||
65: body
|
||||
... skeleton joint names
|
||||
*/
|
||||
}
|
||||
|
||||
function animateLeftHand() {
|
||||
|
@ -357,6 +300,13 @@ var leapHands = (function () {
|
|||
settingsTimer = Script.setInterval(checkSettings, 2000);
|
||||
|
||||
calibrationStatus = UNCALIBRATED;
|
||||
|
||||
{
|
||||
var mapping = Controller.newMapping("LeapmotionTrigger");
|
||||
mapping.from(getLeapMotionLeftTrigger).to(Controller.Standard.LT);
|
||||
mapping.from(getLeapMotionRightTrigger).to(Controller.Standard.RT);
|
||||
mapping.enable();
|
||||
}
|
||||
}
|
||||
|
||||
function moveHands() {
|
||||
|
@ -469,10 +419,17 @@ var leapHands = (function () {
|
|||
hands[h].rotation = handRotation;
|
||||
|
||||
// Set finger joints ...
|
||||
var summed = 0;
|
||||
var closeAngle = 0;
|
||||
for (i = 0; i < NUM_FINGERS; i += 1) {
|
||||
for (j = 0; j < NUM_FINGER_JOINTS; j += 1) {
|
||||
if (fingers[h][i][j].controller !== null) {
|
||||
locRotation = fingers[h][i][j].controller.getLocRotation();
|
||||
var eulers = Quat.safeEulerAngles(locRotation);
|
||||
closeAngle += eulers.x;
|
||||
|
||||
summed++;
|
||||
|
||||
if (i === THUMB) {
|
||||
locRotation = {
|
||||
x: side * locRotation.y,
|
||||
|
@ -496,8 +453,21 @@ var leapHands = (function () {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
hands[h].inactiveCount = 0;
|
||||
if (summed > 0) {
|
||||
closeAngle /= summed;
|
||||
}
|
||||
|
||||
var triggerValue = (-closeAngle - LEAP_TRIGGER_START_ANGLE) / (LEAP_TRIGGER_END_ANGLE - LEAP_TRIGGER_START_ANGLE);
|
||||
triggerValue = Math.max(0.0, Math.min(triggerValue, 1.0));
|
||||
|
||||
if (h == 0) {
|
||||
leftTriggerValue = triggerValue;
|
||||
} else {
|
||||
rightTriggerValue = triggerValue;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
|
@ -509,6 +479,8 @@ var leapHands = (function () {
|
|||
if (handAnimationStateHandlers[h] !== null) {
|
||||
MyAvatar.removeAnimationStateHandler(handAnimationStateHandlers[h]);
|
||||
handAnimationStateHandlers[h] = null;
|
||||
leftTriggerValue = 0.0;
|
||||
rightTriggerValue = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
//
|
||||
// reticleHandRotationTest.js
|
||||
// examples/controllers
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2015/12/15
|
||||
// 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
|
||||
//
|
||||
|
||||
var DEBUGGING = false;
|
||||
|
||||
Math.clamp=function(a,b,c) {
|
||||
return Math.max(b,Math.min(c,a));
|
||||
}
|
||||
|
||||
function length(posA, posB) {
|
||||
var dx = posA.x - posB.x;
|
||||
var dy = posA.y - posB.y;
|
||||
var length = Math.sqrt((dx*dx) + (dy*dy))
|
||||
return length;
|
||||
}
|
||||
|
||||
function moveReticleAbsolute(x, y) {
|
||||
var globalPos = Reticle.getPosition();
|
||||
globalPos.x = x;
|
||||
globalPos.y = y;
|
||||
Reticle.setPosition(globalPos);
|
||||
}
|
||||
|
||||
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);
|
||||
mapping.enable();
|
||||
|
||||
|
||||
|
||||
function debugPrint(message) {
|
||||
if (DEBUGGING) {
|
||||
print(message);
|
||||
}
|
||||
}
|
||||
|
||||
var leftRightBias = 0.0;
|
||||
var filteredRotatedLeft = Vec3.UNIT_NEG_Y;
|
||||
var filteredRotatedRight = Vec3.UNIT_NEG_Y;
|
||||
|
||||
Script.update.connect(function(deltaTime) {
|
||||
|
||||
var poseRight = Controller.getPoseValue(Controller.Standard.RightHand);
|
||||
var poseLeft = Controller.getPoseValue(Controller.Standard.LeftHand);
|
||||
|
||||
// NOTE: hack for now
|
||||
var screenSize = Reticle.maximumPosition;
|
||||
var screenSizeX = screenSize.x;
|
||||
var screenSizeY = screenSize.y;
|
||||
|
||||
var rotatedRight = Vec3.multiplyQbyV(poseRight.rotation, Vec3.UNIT_NEG_Y);
|
||||
var rotatedLeft = Vec3.multiplyQbyV(poseLeft.rotation, Vec3.UNIT_NEG_Y);
|
||||
|
||||
lastRotatedRight = rotatedRight;
|
||||
|
||||
|
||||
// Decide which hand should be controlling the pointer
|
||||
// by comparing which one is moving more, and by
|
||||
// tending to stay with the one moving more.
|
||||
var BIAS_ADJUST_RATE = 0.5;
|
||||
var BIAS_ADJUST_DEADZONE = 0.05;
|
||||
leftRightBias += (Vec3.length(poseRight.angularVelocity) - Vec3.length(poseLeft.angularVelocity)) * BIAS_ADJUST_RATE;
|
||||
if (leftRightBias < BIAS_ADJUST_DEADZONE) {
|
||||
leftRightBias = 0.0;
|
||||
} else if (leftRightBias > (1.0 - BIAS_ADJUST_DEADZONE)) {
|
||||
leftRightBias = 1.0;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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);
|
||||
|
||||
var absolutePitch = rotated.y; // from 1 down to -1 up ... but note: if you rotate down "too far" it starts to go up again...
|
||||
var absoluteYaw = -rotated.x; // from -1 left to 1 right
|
||||
|
||||
var ROTATION_BOUND = 0.6;
|
||||
var clampYaw = Math.clamp(absoluteYaw, -ROTATION_BOUND, ROTATION_BOUND);
|
||||
var clampPitch = Math.clamp(absolutePitch, -ROTATION_BOUND, ROTATION_BOUND);
|
||||
|
||||
// using only from -ROTATION_BOUND to ROTATION_BOUND
|
||||
var xRatio = (clampYaw + ROTATION_BOUND) / (2 * ROTATION_BOUND);
|
||||
var yRatio = (clampPitch + ROTATION_BOUND) / (2 * ROTATION_BOUND);
|
||||
|
||||
var x = screenSizeX * xRatio;
|
||||
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;
|
||||
var angularVelocityMagnitude = Vec3.length(poseLeft.angularVelocity) * (1.0 - leftRightBias) + Vec3.length(poseRight.angularVelocity) * leftRightBias;
|
||||
|
||||
if (!(xRatio == 0.5 && yRatio == 0) && (angularVelocityMagnitude > MINIMUM_CONTROLLER_ANGULAR_VELOCITY)) {
|
||||
moveReticleAbsolute(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
Script.scriptEnding.connect(function(){
|
||||
mapping.disable();
|
||||
});
|
||||
|
||||
|
68
examples/cows/cowEntityScript.js
Normal file
68
examples/cows/cowEntityScript.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
|
||||
// cowEntityScript.js
|
||||
// examples/cows
|
||||
//
|
||||
// Created by Eric Levin on 3/25/16
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// This entity script handles the logic for untipping a cow after it collides with something
|
||||
//
|
||||
// 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 _this = this;
|
||||
_this.COLLISION_COOLDOWN_TIME = 5000;
|
||||
|
||||
|
||||
this.preload = function(entityID) {
|
||||
print("EBL Preload!!");
|
||||
_this.entityID = entityID;
|
||||
_this.mooSound = SoundCache.getSound("https://s3-us-west-1.amazonaws.com/hifi-content/eric/Sounds/moo.wav")
|
||||
_this.mooSoundOptions = {volume: 0.7, loop: false};
|
||||
_this.timeSinceLastCollision = 0;
|
||||
_this.shouldUntipCow = true;
|
||||
}
|
||||
|
||||
this.collisionWithEntity = function(myID, otherID, collisionInfo) {
|
||||
if(_this.shouldUntipCow) {
|
||||
Script.setTimeout(function() {
|
||||
_this.untipCow();
|
||||
_this.shouldUntipCow = true;
|
||||
}, _this.COLLISION_COOLDOWN_TIME);
|
||||
}
|
||||
|
||||
_this.shouldUntipCow = false;
|
||||
|
||||
}
|
||||
|
||||
this.untipCow = function() {
|
||||
// keep yaw but reset pitch and roll
|
||||
var cowProps = Entities.getEntityProperties(_this.entityID, ["rotation", "position"]);
|
||||
var eulerRotation = Quat.safeEulerAngles(cowProps.rotation);
|
||||
eulerRotation.x = 0;
|
||||
eulerRotation.z = 0;
|
||||
var newRotation = Quat.fromVec3Degrees(eulerRotation);
|
||||
Entities.editEntity(_this.entityID, {
|
||||
rotation: newRotation,
|
||||
velocity: {x: 0, y: 0, z: 0},
|
||||
angularVelocity: {x: 0, y: 0, z:0}
|
||||
});
|
||||
|
||||
|
||||
_this.mooSoundOptions.position = cowProps.position;
|
||||
if (!_this.soundInjector) {
|
||||
_this.soundInjector = Audio.playSound(_this.mooSound, _this.mooSoundOptions);
|
||||
} else {
|
||||
_this.soundInjector.setOptions(_this.mooSoundOptions);
|
||||
_this.soundInjector.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
53
examples/cows/cowSpawner.js
Normal file
53
examples/cows/cowSpawner.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
|
||||
// cowSpawner.js
|
||||
// examples/cows
|
||||
//
|
||||
// Created by Eric Levin on 3/25/16
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// This spawns a cow which will untip itself
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var orientation = MyAvatar.orientation;
|
||||
orientation = Quat.safeEulerAngles(orientation);
|
||||
orientation.x = 0;
|
||||
orientation = Quat.fromVec3Degrees(orientation);
|
||||
var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation)));
|
||||
|
||||
|
||||
var SCRIPT_URL = Script.resolvePath("cowEntityScript.js?");
|
||||
var cow = Entities.addEntity({
|
||||
type: "Model",
|
||||
modelURL: "http://hifi-content.s3.amazonaws.com/DomainContent/production/cow/newMooCow.fbx",
|
||||
name: "playa_model_throwinCow",
|
||||
position: center,
|
||||
animation: {
|
||||
currentFrame: 278,
|
||||
running: true,
|
||||
url: "http://hifi-content.s3.amazonaws.com/DomainContent/Junkyard/Playa/newMooCow.fbx"
|
||||
},
|
||||
dimensions: {
|
||||
x: 0.739,
|
||||
y: 1.613,
|
||||
z: 2.529
|
||||
},
|
||||
dynamic: true,
|
||||
gravity: {
|
||||
x: 0,
|
||||
y: -5,
|
||||
z: 0
|
||||
},
|
||||
shapeType: "box",
|
||||
script: SCRIPT_URL,
|
||||
userData: "{\"grabbableKey\":{\"grabbable\":true}}"
|
||||
});
|
||||
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(cow);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
60
examples/data_visualization/photo_sphere.js
Normal file
60
examples/data_visualization/photo_sphere.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
// photo_sphere.js
|
||||
//
|
||||
// Created by James B. Pollack @imgntn on 3/11/2015
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// This script creates a photo sphere around you.
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var photoSphere, light;
|
||||
|
||||
//equirectangular
|
||||
var url = 'http://hifi-content.s3.amazonaws.com/james/projection_objects/IMG_9167.JPG';
|
||||
|
||||
var MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/projection_objects/photosphere2.fbx';
|
||||
|
||||
function createPhotoSphere() {
|
||||
|
||||
var textureString = 'photo:"' + url + '"'
|
||||
|
||||
var properties = {
|
||||
type: 'Model',
|
||||
modelURL: MODEL_URL,
|
||||
name: 'hifi-photo-sphere',
|
||||
dimensions: {
|
||||
x: 32,
|
||||
y: 32,
|
||||
z: 32
|
||||
},
|
||||
position: MyAvatar.position,
|
||||
textures: textureString
|
||||
}
|
||||
photoSphere = Entities.addEntity(properties);
|
||||
}
|
||||
|
||||
function createLight() {
|
||||
var properties = {
|
||||
name: 'hifi-photo-sphere-light',
|
||||
type: 'Light',
|
||||
dimensions: {
|
||||
x: 36,
|
||||
y: 36,
|
||||
z: 36,
|
||||
},
|
||||
intensity: 4.0,
|
||||
falloffRadius: 22,
|
||||
position: MyAvatar.position
|
||||
}
|
||||
light = Entities.addEntity(properties);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(photoSphere);
|
||||
Entities.deleteEntity(light);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
createPhotoSphere();
|
||||
createLight();
|
|
@ -13,7 +13,6 @@ Script.load("progress.js");
|
|||
Script.load("edit.js");
|
||||
Script.load("marketplace.js");
|
||||
Script.load("selectAudioDevice.js");
|
||||
Script.load("inspect.js");
|
||||
Script.load("notifications.js");
|
||||
Script.load("users.js");
|
||||
Script.load("controllers/handControllerGrab.js");
|
||||
|
@ -21,4 +20,5 @@ Script.load("controllers/squeezeHands.js");
|
|||
Script.load("grab.js");
|
||||
Script.load("directory.js");
|
||||
Script.load("dialTone.js");
|
||||
Script.load("attachedEntitiesManager.js");
|
||||
// Script.load("attachedEntitiesManager.js");
|
||||
Script.load("depthReticle.js");
|
||||
|
|
174
examples/depthReticle.js
Normal file
174
examples/depthReticle.js
Normal file
|
@ -0,0 +1,174 @@
|
|||
// depthReticle.js
|
||||
// examples
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2/23/16.
|
||||
// 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
|
||||
//
|
||||
|
||||
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 = 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;
|
||||
|
||||
var lastMouseMoveOrClick = 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;
|
||||
|
||||
function showReticleOnMouseClick() {
|
||||
Reticle.visible = true;
|
||||
lastMouseMoveOrClick = Date.now(); // move or click
|
||||
}
|
||||
|
||||
Controller.mousePressEvent.connect(showReticleOnMouseClick);
|
||||
Controller.mouseDoublePressEvent.connect(showReticleOnMouseClick);
|
||||
|
||||
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 - lastMouseMoveOrClick)); // 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastMouseMoveOrClick = 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 - lastMouseMoveOrClick;
|
||||
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 && Reticle.visible) {
|
||||
var newDesiredDepth = desiredDepth;
|
||||
lastDepthCheckTime = now;
|
||||
var reticlePosition = Reticle.position;
|
||||
|
||||
// first check the 2D Overlays
|
||||
if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(reticlePosition)) {
|
||||
newDesiredDepth = APPARENT_2D_OVERLAY_DEPTH;
|
||||
} else {
|
||||
var pickRay = Camera.computePickRay(reticlePosition.x, reticlePosition.y);
|
||||
|
||||
// Then check the 3D overlays
|
||||
var result = Overlays.findRayIntersection(pickRay);
|
||||
|
||||
if (!result.intersects) {
|
||||
// finally check the entities
|
||||
result = Entities.findRayIntersection(pickRay, true);
|
||||
}
|
||||
|
||||
// If either the overlays or entities intersect, then set the reticle depth to
|
||||
// the distance of intersection
|
||||
if (result.intersects) {
|
||||
newDesiredDepth = result.distance;
|
||||
} else {
|
||||
// if nothing intersects... set the depth to some sufficiently large 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
|
||||
}
|
||||
});
|
|
@ -53,7 +53,6 @@ var toolBar = (function() {
|
|||
browseDirectoryButton;
|
||||
|
||||
function initialize() {
|
||||
ToolBar.SPACING = 16;
|
||||
toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.directory.toolbar", function(windowDimensions, toolbar) {
|
||||
return {
|
||||
x: windowDimensions.x - 8 - toolbar.width,
|
||||
|
@ -61,11 +60,18 @@ var toolBar = (function() {
|
|||
};
|
||||
});
|
||||
browseDirectoryButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "directory.svg",
|
||||
imageURL: toolIconUrl + "directory-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
width: Tool.IMAGE_WIDTH,
|
||||
height: Tool.IMAGE_HEIGHT
|
||||
},
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
visible: true,
|
||||
showButtonDown: true
|
||||
});
|
||||
|
||||
toolBar.showTool(browseDirectoryButton, true);
|
||||
|
|
248
examples/edit.js
248
examples/edit.js
|
@ -38,6 +38,10 @@ var lightOverlayManager = new LightOverlayManager();
|
|||
var cameraManager = new CameraManager();
|
||||
|
||||
var grid = Grid();
|
||||
gridTool = GridTool({
|
||||
horizontalGrid: grid
|
||||
});
|
||||
gridTool.setVisible(false);
|
||||
|
||||
var entityListTool = EntityListTool();
|
||||
|
||||
|
@ -85,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."
|
||||
|
||||
|
@ -173,7 +180,6 @@ var toolBar = (function() {
|
|||
newTextButton,
|
||||
newWebButton,
|
||||
newZoneButton,
|
||||
newPolyVoxButton,
|
||||
newParticleButton
|
||||
|
||||
function initialize() {
|
||||
|
@ -184,10 +190,8 @@ var toolBar = (function() {
|
|||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
activeButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "edit-status.svg",
|
||||
imageURL: toolIconUrl + "edit-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
|
@ -201,7 +205,7 @@ var toolBar = (function() {
|
|||
}, true, false);
|
||||
|
||||
newModelButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "upload.svg",
|
||||
imageURL: toolIconUrl + "upload-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
|
@ -211,11 +215,12 @@ var toolBar = (function() {
|
|||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newCubeButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "add-cube.svg",
|
||||
imageURL: toolIconUrl + "cube-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
|
@ -225,11 +230,12 @@ var toolBar = (function() {
|
|||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newSphereButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "add-sphere.svg",
|
||||
imageURL: toolIconUrl + "sphere-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
|
@ -239,11 +245,12 @@ var toolBar = (function() {
|
|||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newLightButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "light.svg",
|
||||
imageURL: toolIconUrl + "light-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
|
@ -253,11 +260,12 @@ var toolBar = (function() {
|
|||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newTextButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "add-text.svg",
|
||||
imageURL: toolIconUrl + "text-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
|
@ -267,62 +275,52 @@ var toolBar = (function() {
|
|||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newWebButton = toolBar.addTool({
|
||||
imageURL: "https://hifi-public.s3.amazonaws.com/images/www.svg",
|
||||
imageURL: toolIconUrl + "web-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 128,
|
||||
height: 128
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
width: Tool.IMAGE_WIDTH,
|
||||
height: Tool.IMAGE_HEIGHT
|
||||
},
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newZoneButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "zonecube_text.svg",
|
||||
imageURL: toolIconUrl + "zone-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: 128,
|
||||
width: 128,
|
||||
height: 128
|
||||
},
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newPolyVoxButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "polyvox.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 256,
|
||||
height: 256
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
width: Tool.IMAGE_WIDTH,
|
||||
height: Tool.IMAGE_HEIGHT
|
||||
},
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
newParticleButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "particle.svg",
|
||||
imageURL: toolIconUrl + "particle-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 256,
|
||||
height: 256
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
width: Tool.IMAGE_WIDTH,
|
||||
height: Tool.IMAGE_HEIGHT
|
||||
},
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
showButtonDown: true,
|
||||
visible: false
|
||||
});
|
||||
|
||||
|
@ -338,10 +336,11 @@ var toolBar = (function() {
|
|||
if (active && !Entities.canAdjustLocks()) {
|
||||
Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG);
|
||||
} else {
|
||||
Messages.sendLocalMessage("edit-events", JSON.stringify({enabled: active}));
|
||||
isActive = active;
|
||||
if (!isActive) {
|
||||
entityListTool.setVisible(false);
|
||||
// gridTool.setVisible(false);
|
||||
gridTool.setVisible(false);
|
||||
grid.setEnabled(false);
|
||||
propertiesTool.setVisible(false);
|
||||
selectionManager.clearSelections();
|
||||
|
@ -349,7 +348,7 @@ var toolBar = (function() {
|
|||
} else {
|
||||
hasShownPropertiesTool = false;
|
||||
entityListTool.setVisible(true);
|
||||
// gridTool.setVisible(true);
|
||||
gridTool.setVisible(true);
|
||||
grid.setEnabled(true);
|
||||
propertiesTool.setVisible(true);
|
||||
Window.setFocus();
|
||||
|
@ -371,7 +370,6 @@ var toolBar = (function() {
|
|||
toolBar.showTool(newTextButton, doShow);
|
||||
toolBar.showTool(newWebButton, doShow);
|
||||
toolBar.showTool(newZoneButton, doShow);
|
||||
toolBar.showTool(newPolyVoxButton, doShow);
|
||||
toolBar.showTool(newParticleButton, doShow);
|
||||
};
|
||||
|
||||
|
@ -413,7 +411,6 @@ var toolBar = (function() {
|
|||
return entityID;
|
||||
}
|
||||
|
||||
var newModelButtonDown = false;
|
||||
that.mousePressEvent = function(event) {
|
||||
var clickedOverlay,
|
||||
url,
|
||||
|
@ -434,14 +431,14 @@ var toolBar = (function() {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Handle these two buttons in the mouseRelease event handler so that we don't suppress a mouseRelease event from
|
||||
// occurring when showing a modal dialog.
|
||||
if (newModelButton === toolBar.clicked(clickedOverlay)) {
|
||||
newModelButtonDown = true;
|
||||
url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]);
|
||||
if (url !== null && url !== "") {
|
||||
addModel(url);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (newCubeButton === toolBar.clicked(clickedOverlay)) {
|
||||
createNewEntity({
|
||||
type: "Box",
|
||||
|
@ -543,92 +540,6 @@ var toolBar = (function() {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (newPolyVoxButton === toolBar.clicked(clickedOverlay)) {
|
||||
var polyVoxId = createNewEntity({
|
||||
type: "PolyVox",
|
||||
dimensions: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
z: 10
|
||||
},
|
||||
voxelVolumeSize: {
|
||||
x: 16,
|
||||
y: 16,
|
||||
z: 16
|
||||
},
|
||||
voxelSurfaceStyle: 2
|
||||
});
|
||||
for (var x = 1; x <= 14; x++) {
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: x,
|
||||
y: 1,
|
||||
z: 1
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: x,
|
||||
y: 14,
|
||||
z: 1
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: x,
|
||||
y: 1,
|
||||
z: 14
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: x,
|
||||
y: 14,
|
||||
z: 14
|
||||
}, 255);
|
||||
}
|
||||
for (var y = 2; y <= 13; y++) {
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 1,
|
||||
y: y,
|
||||
z: 1
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 14,
|
||||
y: y,
|
||||
z: 1
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 1,
|
||||
y: y,
|
||||
z: 14
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 14,
|
||||
y: y,
|
||||
z: 14
|
||||
}, 255);
|
||||
}
|
||||
for (var z = 2; z <= 13; z++) {
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: z
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 14,
|
||||
y: 1,
|
||||
z: z
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 1,
|
||||
y: 14,
|
||||
z: z
|
||||
}, 255);
|
||||
Entities.setVoxel(polyVoxId, {
|
||||
x: 14,
|
||||
y: 14,
|
||||
z: z
|
||||
}, 255);
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (newParticleButton === toolBar.clicked(clickedOverlay)) {
|
||||
createNewEntity({
|
||||
type: "ParticleEffect",
|
||||
|
@ -648,26 +559,8 @@ var toolBar = (function() {
|
|||
return false;
|
||||
};
|
||||
|
||||
that.mouseReleaseEvent = function(event) {
|
||||
var handled = false;
|
||||
if (newModelButtonDown) {
|
||||
var clickedOverlay = Overlays.getOverlayAtPoint({
|
||||
x: event.x,
|
||||
y: event.y
|
||||
});
|
||||
if (newModelButton === toolBar.clicked(clickedOverlay)) {
|
||||
url = Window.prompt("Model URL", modelURLs[Math.floor(Math.random() * modelURLs.length)]);
|
||||
if (url !== null && url !== "") {
|
||||
addModel(url);
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
newModelButtonDown = false;
|
||||
|
||||
|
||||
return handled;
|
||||
that.mouseReleaseEvent = function (event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Window.domainChanged.connect(function() {
|
||||
|
@ -707,13 +600,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;
|
||||
|
||||
|
@ -754,7 +666,8 @@ function findClickedEntity(event) {
|
|||
var foundEntity = result.entityID;
|
||||
return {
|
||||
pickRay: pickRay,
|
||||
entityID: foundEntity
|
||||
entityID: foundEntity,
|
||||
intersection: result.intersection
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -922,6 +835,7 @@ function mouseReleaseEvent(event) {
|
|||
}
|
||||
|
||||
function mouseClickEvent(event) {
|
||||
var wantDebug = false;
|
||||
if (isActive && event.isLeftButton) {
|
||||
var result = findClickedEntity(event);
|
||||
if (result === null) {
|
||||
|
@ -936,11 +850,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
|
||||
|
@ -977,8 +895,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)) {
|
||||
|
@ -992,6 +911,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;
|
||||
|
@ -1562,10 +1484,10 @@ PropertiesTool = function(opts) {
|
|||
selections.push(entity);
|
||||
}
|
||||
data.selections = selections;
|
||||
webView.eventBridge.emitScriptEvent(JSON.stringify(data));
|
||||
webView.emitScriptEvent(JSON.stringify(data));
|
||||
});
|
||||
|
||||
webView.eventBridge.webEventReceived.connect(function(data) {
|
||||
webView.webEventReceived.connect(function(data) {
|
||||
data = JSON.parse(data);
|
||||
if (data.type == "print") {
|
||||
if (data.message) {
|
||||
|
@ -1865,9 +1787,11 @@ PopupMenu = function() {
|
|||
return this;
|
||||
};
|
||||
|
||||
|
||||
var propertyMenu = PopupMenu();
|
||||
|
||||
propertyMenu.onSelectMenuItem = function(name) {
|
||||
|
||||
if (propertyMenu.marketplaceID) {
|
||||
showMarketplace(propertyMenu.marketplaceID);
|
||||
}
|
||||
|
@ -1878,7 +1802,7 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace");
|
|||
propertiesTool = PropertiesTool();
|
||||
var particleExplorerTool = ParticleExplorerTool();
|
||||
var selectedParticleEntity = 0;
|
||||
entityListTool.webView.eventBridge.webEventReceived.connect(function(data) {
|
||||
entityListTool.webView.webEventReceived.connect(function(data) {
|
||||
var data = JSON.parse(data);
|
||||
if (data.type == "selectionUpdate") {
|
||||
var ids = data.entityIds;
|
||||
|
@ -1899,10 +1823,10 @@ entityListTool.webView.eventBridge.webEventReceived.connect(function(data) {
|
|||
selectedParticleEntity = ids[0];
|
||||
particleExplorerTool.setActiveParticleEntity(ids[0]);
|
||||
|
||||
particleExplorerTool.webView.eventBridge.webEventReceived.connect(function(data) {
|
||||
particleExplorerTool.webView.webEventReceived.connect(function(data) {
|
||||
var data = JSON.parse(data);
|
||||
if (data.messageType === "page_loaded") {
|
||||
particleExplorerTool.webView.eventBridge.emitScriptEvent(JSON.stringify(particleData));
|
||||
particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -1911,4 +1835,4 @@ entityListTool.webView.eventBridge.webEventReceived.connect(function(data) {
|
|||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
26
examples/entityScripts/changeColorOnEnterLeave.js
Normal file
26
examples/entityScripts/changeColorOnEnterLeave.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// changeColorOnEnterLeave.js
|
||||
// examples/entityScripts
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 3/31/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
|
||||
//
|
||||
|
||||
(function(){
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
this.enterEntity = function(myID) {
|
||||
print("enterEntity() myID:" + myID);
|
||||
Entities.editEntity(myID, { color: { red: getRandomInt(128,255), green: getRandomInt(128,255), blue: getRandomInt(128,255)} });
|
||||
};
|
||||
|
||||
this.leaveEntity = function(myID) {
|
||||
print("leaveEntity() myID:" + myID);
|
||||
Entities.editEntity(myID, { color: { red: getRandomInt(128,255), green: getRandomInt(128,255), blue: getRandomInt(128,255)} });
|
||||
};
|
||||
})
|
339
examples/entityScripts/lightningEntity.js
Normal file
339
examples/entityScripts/lightningEntity.js
Normal 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();
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
(function() {
|
||||
Script.include("../../libraries/virtualBaton.js");
|
||||
Script.include("../../libraries/utils.js");
|
||||
|
||||
var _this = this;
|
||||
|
||||
|
||||
this.startUpdate = function() {
|
||||
print("EBL START UPDATE");
|
||||
Entities.editEntity(_this.batonOwnerIndicator, {
|
||||
visible: true
|
||||
});
|
||||
|
||||
// Change color of box
|
||||
Entities.editEntity(_this.entityID, {
|
||||
color: randomColor()
|
||||
});
|
||||
|
||||
_this.position = Entities.getEntityProperties(_this.entityID, "position").position;
|
||||
_this.debugLightProperties.position = Vec3.sum(_this.position, {x: 0, y: 1, z: 0});
|
||||
_this.debugLightProperties.color = randomColor();
|
||||
var debugLight = Entities.addEntity(_this.debugLightProperties);
|
||||
Script.setTimeout(function() {
|
||||
Entities.deleteEntity(debugLight);
|
||||
}, 500);
|
||||
|
||||
}
|
||||
|
||||
this.maybeClaim = function() {
|
||||
print("EBL MAYBE CLAIM");
|
||||
if (_this.isBatonOwner === true) {
|
||||
_this.isBatonOwner = false;
|
||||
}
|
||||
Entities.editEntity(_this.batonOwnerIndicator, {
|
||||
visible: false
|
||||
});
|
||||
baton.claim(_this.startUpdate, _this.maybeClaim);
|
||||
}
|
||||
|
||||
this.unload = function() {
|
||||
print("EBL UNLOAD");
|
||||
baton.unload();
|
||||
Entities.deleteEntity(_this.batonOwnerIndicator);
|
||||
}
|
||||
|
||||
|
||||
this.preload = function(entityID) {
|
||||
print("EBL Preload!!");
|
||||
_this.entityID = entityID;
|
||||
_this.setupDebugEntities();
|
||||
|
||||
baton = virtualBaton({
|
||||
batonName: "batonSimpleEntityScript:" + _this.entityID
|
||||
});
|
||||
_this.isBatonOwner = false;
|
||||
_this.maybeClaim();
|
||||
|
||||
}
|
||||
|
||||
|
||||
this.setupDebugEntities = function() {
|
||||
_this.batonOwnerIndicator = Entities.addEntity({
|
||||
type: "Box",
|
||||
color: {
|
||||
red: 200,
|
||||
green: 10,
|
||||
blue: 200
|
||||
},
|
||||
position: Vec3.sum(MyAvatar.position, {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
}),
|
||||
dimensions: {
|
||||
x: 0.5,
|
||||
y: 1,
|
||||
z: 0
|
||||
},
|
||||
parentID: MyAvatar.sessionUUID,
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
_this.debugLightProperties = {
|
||||
type: "Light",
|
||||
name: "hifi-baton-light",
|
||||
dimensions: {
|
||||
x: 10,
|
||||
y: 10,
|
||||
z: 10
|
||||
},
|
||||
falloffRadius: 3,
|
||||
intensity: 20,
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
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)));
|
||||
|
||||
// Math.random ensures no caching of script
|
||||
var SCRIPT_URL = Script.resolvePath("batonSimpleEntityScript.js");
|
||||
|
||||
var batonBox = Entities.addEntity({
|
||||
type: "Box",
|
||||
name: "hifi-baton-entity",
|
||||
color: {
|
||||
red: 200,
|
||||
green: 200,
|
||||
blue: 200
|
||||
},
|
||||
position: center,
|
||||
dimensions: {
|
||||
x: 0.1,
|
||||
y: 0.1,
|
||||
z: 0.1
|
||||
},
|
||||
script: SCRIPT_URL
|
||||
});
|
||||
|
||||
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(batonBox);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
106
examples/fireflies/firefly.js
Normal file
106
examples/fireflies/firefly.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// Created by Philip Rosedale on March 7, 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
|
||||
//
|
||||
// A firefly which is animated by passerbys. It's physical, no gravity, periodic forces applied.
|
||||
// If a firefly is found to
|
||||
//
|
||||
|
||||
(function () {
|
||||
var entityID,
|
||||
timeoutID = null,
|
||||
properties,
|
||||
shouldSimulate = false,
|
||||
ACTIVE_CHECK_INTERVAL = 100, // milliseconds
|
||||
INACTIVE_CHECK_INTERVAL = 1000, // milliseconds
|
||||
MAX_DISTANCE_TO_SIMULATE = 20, // meters
|
||||
LIGHT_LIFETIME = 1400, // milliseconds a firefly light will stay alive
|
||||
BUMP_SPEED = 1.5, // average velocity given by a bump
|
||||
BUMP_CHANCE = 0.33,
|
||||
MIN_SPEED = 0.125, // below this speed, firefly gets a new bump
|
||||
SPIN_SPEED = 3.5,
|
||||
BRIGHTNESS = 0.25,
|
||||
wantDebug = false
|
||||
|
||||
function randomVector(size) {
|
||||
return { x: (Math.random() - 0.5) * size,
|
||||
y: (Math.random() - 0.5) * size,
|
||||
z: (Math.random() - 0.5) * size };
|
||||
}
|
||||
|
||||
function printDebug(message) {
|
||||
if (wantDebug) {
|
||||
print(message);
|
||||
}
|
||||
}
|
||||
|
||||
function maybe() {
|
||||
properties = Entities.getEntityProperties(entityID);
|
||||
var speed = Vec3.length(properties.velocity);
|
||||
var distance = Vec3.distance(MyAvatar.position, properties.position);
|
||||
printDebug("maybe: speed: " + speed + ", distance: " + distance);
|
||||
if (shouldSimulate) {
|
||||
// We are simulating this firefly, so do stuff:
|
||||
if (distance > MAX_DISTANCE_TO_SIMULATE) {
|
||||
shouldSimulate = false;
|
||||
} else if ((speed < MIN_SPEED) && (Math.random() < BUMP_CHANCE)) {
|
||||
bump();
|
||||
makeLight();
|
||||
}
|
||||
} else if (Vec3.length(properties.velocity) == 0.0) {
|
||||
// We've found a firefly that is not being simulated, so maybe take it over
|
||||
if (distance < MAX_DISTANCE_TO_SIMULATE) {
|
||||
shouldSimulate = true;
|
||||
}
|
||||
}
|
||||
timeoutID = Script.setTimeout(maybe, (shouldSimulate == true) ? ACTIVE_CHECK_INTERVAL : INACTIVE_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
function bump() {
|
||||
// Give the firefly a little brownian hop
|
||||
printDebug("bump!");
|
||||
var velocity = randomVector(BUMP_SPEED);
|
||||
if (velocity.y < 0.0) { velocity.y *= -1.0 };
|
||||
Entities.editEntity(entityID, { velocity: velocity,
|
||||
angularVelocity: randomVector(SPIN_SPEED) });
|
||||
}
|
||||
|
||||
function makeLight() {
|
||||
printDebug("make light!");
|
||||
// create a light attached to the firefly that lives for a while
|
||||
Entities.addEntity({
|
||||
type: "Light",
|
||||
name: "firefly light",
|
||||
intensity: 4.0 * BRIGHTNESS,
|
||||
falloffRadius: 8.0 * BRIGHTNESS,
|
||||
dimensions: {
|
||||
x: 30 * BRIGHTNESS,
|
||||
y: 30 * BRIGHTNESS,
|
||||
z: 30 * BRIGHTNESS
|
||||
},
|
||||
position: Vec3.sum(properties.position, { x: 0, y: 0.2, z: 0 }),
|
||||
parentID: entityID,
|
||||
color: {
|
||||
red: 150 + Math.random() * 100,
|
||||
green: 100 + Math.random() * 50,
|
||||
blue: 150 + Math.random() * 100
|
||||
},
|
||||
lifetime: LIGHT_LIFETIME / 1000
|
||||
});
|
||||
}
|
||||
|
||||
this.preload = function (givenEntityID) {
|
||||
printDebug("preload firefly...");
|
||||
entityID = givenEntityID;
|
||||
timeoutID = Script.setTimeout(maybe, ACTIVE_CHECK_INTERVAL);
|
||||
};
|
||||
this.unload = function () {
|
||||
printDebug("unload firefly...");
|
||||
if (timeoutID !== undefined) {
|
||||
Script.clearTimeout(timeoutID);
|
||||
}
|
||||
};
|
||||
})
|
77
examples/fireflies/makeFireflies.js
Normal file
77
examples/fireflies/makeFireflies.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// Created by Philip Rosedale on March 7, 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
|
||||
//
|
||||
// Make some fireflies
|
||||
//
|
||||
|
||||
var SIZE = 0.05;
|
||||
//var ENTITY_URL = "file:///c:/users/dev/philip/examples/fireflies/firefly.js?"+Math.random()
|
||||
var ENTITY_URL = "https://s3.amazonaws.com/hifi-public/scripts/fireflies/firefly.js"
|
||||
|
||||
var RATE_PER_SECOND = 50; // The entity server will drop data if we create things too fast.
|
||||
var SCRIPT_INTERVAL = 100;
|
||||
var LIFETIME = 120;
|
||||
|
||||
var NUMBER_TO_CREATE = 100;
|
||||
|
||||
var GRAVITY = { x: 0, y: -1.0, z: 0 };
|
||||
|
||||
var DAMPING = 0.5;
|
||||
var ANGULAR_DAMPING = 0.5;
|
||||
|
||||
var collidable = true;
|
||||
var gravity = true;
|
||||
|
||||
var RANGE = 10;
|
||||
var HEIGHT = 3;
|
||||
var HOW_FAR_IN_FRONT_OF_ME = 1.0;
|
||||
|
||||
var totalCreated = 0;
|
||||
|
||||
var center = Vec3.sum(MyAvatar.position, Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getFront(Camera.orientation)));
|
||||
|
||||
|
||||
function randomVector(range) {
|
||||
return {
|
||||
x: (Math.random() - 0.5) * range.x,
|
||||
y: (Math.random() - 0.5) * range.y,
|
||||
z: (Math.random() - 0.5) * range.z
|
||||
}
|
||||
}
|
||||
|
||||
Vec3.print("Center: ", center);
|
||||
|
||||
Script.setInterval(function () {
|
||||
if (!Entities.serversExist() || !Entities.canRez() || (totalCreated > NUMBER_TO_CREATE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var numToCreate = RATE_PER_SECOND * (SCRIPT_INTERVAL / 1000.0);
|
||||
for (var i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) {
|
||||
|
||||
var position = Vec3.sum(center, randomVector({ x: RANGE, y: HEIGHT, z: RANGE }));
|
||||
position.y += HEIGHT / 2.0;
|
||||
|
||||
Entities.addEntity({
|
||||
type: "Box",
|
||||
name: "firefly",
|
||||
position: position,
|
||||
dimensions: { x: SIZE, y: SIZE, z: SIZE },
|
||||
color: { red: 150 + Math.random() * 100, green: 100 + Math.random() * 50, blue: 0 },
|
||||
damping: DAMPING,
|
||||
angularDamping: ANGULAR_DAMPING,
|
||||
gravity: (gravity ? GRAVITY : { x: 0, y: 0, z: 0}),
|
||||
dynamic: collidable,
|
||||
script: ENTITY_URL,
|
||||
lifetime: LIFETIME
|
||||
});
|
||||
|
||||
totalCreated++;
|
||||
print("Firefly #" + totalCreated);
|
||||
}
|
||||
}, SCRIPT_INTERVAL);
|
||||
|
|
@ -49,6 +49,8 @@ var IDENTITY_QUAT = {
|
|||
};
|
||||
var ACTION_TTL = 10; // seconds
|
||||
|
||||
var enabled = true;
|
||||
|
||||
function getTag() {
|
||||
return "grab-" + MyAvatar.sessionUUID;
|
||||
}
|
||||
|
@ -306,6 +308,10 @@ Grabber.prototype.computeNewGrabPlane = function() {
|
|||
}
|
||||
|
||||
Grabber.prototype.pressEvent = function(event) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) {
|
||||
return;
|
||||
}
|
||||
|
@ -367,6 +373,10 @@ Grabber.prototype.pressEvent = function(event) {
|
|||
|
||||
beacon.updatePosition(this.startPosition);
|
||||
|
||||
if(!entityIsGrabbedByOther(this.entityID)){
|
||||
this.moveEvent(event);
|
||||
}
|
||||
|
||||
// TODO: play sounds again when we aren't leaking AudioInjector threads
|
||||
//Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME });
|
||||
}
|
||||
|
@ -559,8 +569,29 @@ function keyReleaseEvent(event) {
|
|||
grabber.keyReleaseEvent(event);
|
||||
}
|
||||
|
||||
function editEvent(channel, message, sender, localOnly) {
|
||||
if (channel != "edit-events") {
|
||||
return;
|
||||
}
|
||||
if (sender != MyAvatar.sessionUUID) {
|
||||
return;
|
||||
}
|
||||
if (!localOnly) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
if ("enabled" in data) {
|
||||
enabled = !data["enabled"];
|
||||
}
|
||||
} catch(e) {
|
||||
}
|
||||
}
|
||||
|
||||
Controller.mousePressEvent.connect(pressEvent);
|
||||
Controller.mouseMoveEvent.connect(moveEvent);
|
||||
Controller.mouseReleaseEvent.connect(releaseEvent);
|
||||
Controller.keyPressEvent.connect(keyPressEvent);
|
||||
Controller.keyReleaseEvent.connect(keyReleaseEvent);
|
||||
Messages.subscribe("edit-events");
|
||||
Messages.messageReceived.connect(editEvent);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,285 +0,0 @@
|
|||
//
|
||||
// hmdControls.js
|
||||
// examples
|
||||
//
|
||||
// Created by Sam Gondelman on 6/17/15
|
||||
// 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
|
||||
//
|
||||
|
||||
var MOVE_DISTANCE = 2.0;
|
||||
var PITCH_INCREMENT = 0.5; // degrees
|
||||
var pitchChange = 0; // degrees
|
||||
var YAW_INCREMENT = 0.5; // degrees
|
||||
var VR_YAW_INCREMENT = 15.0; // degrees
|
||||
var yawChange = 0;
|
||||
var BOOM_SPEED = 0.5;
|
||||
var THRESHOLD = 0.2;
|
||||
|
||||
var CAMERA_UPDATE_TIME = 0.5;
|
||||
var yawTimer = CAMERA_UPDATE_TIME;
|
||||
|
||||
var shifted = false;
|
||||
var SHIFT_UPDATE_TIME = 0.5;
|
||||
var shiftTimer = SHIFT_UPDATE_TIME;
|
||||
var SHIFT_MAG = 4.0;
|
||||
|
||||
var warpActive = false;
|
||||
var WARP_UPDATE_TIME = .5;
|
||||
var warpTimer = WARP_UPDATE_TIME;
|
||||
|
||||
var warpPosition = { x: 0, y: 0, z: 0 };
|
||||
|
||||
var WARP_SPHERE_SIZE = 1;
|
||||
var warpSphere = Overlays.addOverlay("sphere", {
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
size: WARP_SPHERE_SIZE,
|
||||
color: { red: 0, green: 255, blue: 0 },
|
||||
alpha: 1.0,
|
||||
solid: true,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
var WARP_LINE_HEIGHT = 10;
|
||||
var warpLine = Overlays.addOverlay("line3d", {
|
||||
start: { x: 0, y: 0, z:0 },
|
||||
end: { x: 0, y: 0, z: 0 },
|
||||
color: { red: 0, green: 255, blue: 255},
|
||||
alpha: 1,
|
||||
lineWidth: 5,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
var velocity = { x: 0, y: 0, z: 0 };
|
||||
var VERY_LONG_TIME = 1000000.0;
|
||||
|
||||
var active = HMD.active;
|
||||
var prevVRMode = HMD.active;
|
||||
|
||||
var hmdControls = (function () {
|
||||
|
||||
function onKeyPressEvent(event) {
|
||||
if (event.text == 'g' && event.isMeta) {
|
||||
active = !active;
|
||||
}
|
||||
}
|
||||
|
||||
function findAction(name) {
|
||||
var actions = Controller.getAllActions();
|
||||
for (var i = 0; i < actions.length; i++) {
|
||||
if (actions[i].actionName == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// If the action isn't found, it will default to the first available action
|
||||
return 0;
|
||||
}
|
||||
|
||||
function onActionEvent(action, state) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
if (state < THRESHOLD) {
|
||||
if (action == findAction("YAW_LEFT") || action == findAction("YAW_RIGHT")) {
|
||||
yawTimer = CAMERA_UPDATE_TIME;
|
||||
} else if (action == findAction("PITCH_UP") || action == findAction("PITCH_DOWN")) {
|
||||
pitchTimer = CAMERA_UPDATE_TIME;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case findAction("LONGITUDINAL_BACKWARD"):
|
||||
var direction = {x: 0.0, y: 0.0, z:1.0};
|
||||
direction = Vec3.multiply(Vec3.normalize(direction), shifted ? SHIFT_MAG * MOVE_DISTANCE : MOVE_DISTANCE);
|
||||
velocity = Vec3.sum(velocity, direction);
|
||||
break;
|
||||
case findAction("LONGITUDINAL_FORWARD"):
|
||||
var direction = {x: 0.0, y: 0.0, z:-1.0};
|
||||
direction = Vec3.multiply(Vec3.normalize(direction), shifted ? SHIFT_MAG * MOVE_DISTANCE : MOVE_DISTANCE);
|
||||
velocity = Vec3.sum(velocity, direction);
|
||||
break;
|
||||
case findAction("LATERAL_LEFT"):
|
||||
var direction = {x:-1.0, y: 0.0, z: 0.0}
|
||||
direction = Vec3.multiply(Vec3.normalize(direction), shifted ? SHIFT_MAG * MOVE_DISTANCE : MOVE_DISTANCE);
|
||||
velocity = Vec3.sum(velocity, direction);
|
||||
break;
|
||||
case findAction("LATERAL_RIGHT"):
|
||||
var direction = {x:1.0, y: 0.0, z: 0.0};
|
||||
direction = Vec3.multiply(Vec3.normalize(direction), shifted ? SHIFT_MAG * MOVE_DISTANCE : MOVE_DISTANCE);
|
||||
velocity = Vec3.sum(velocity, direction);
|
||||
break;
|
||||
case findAction("VERTICAL_DOWN"):
|
||||
var direction = {x: 0.0, y: -1.0, z: 0.0};
|
||||
direction = Vec3.multiply(Vec3.normalize(direction), shifted ? SHIFT_MAG * MOVE_DISTANCE : MOVE_DISTANCE);
|
||||
velocity = Vec3.sum(velocity, direction);
|
||||
break;
|
||||
case findAction("VERTICAL_UP"):
|
||||
var direction = {x: 0.0, y: 1.0, z: 0.0};
|
||||
direction = Vec3.multiply(Vec3.normalize(direction), shifted ? SHIFT_MAG * MOVE_DISTANCE : MOVE_DISTANCE);
|
||||
velocity = Vec3.sum(velocity, direction);
|
||||
break;
|
||||
case findAction("YAW_LEFT"):
|
||||
if (yawTimer < 0.0 && HMD.active) {
|
||||
yawChange = yawChange + (shifted ? SHIFT_MAG * VR_YAW_INCREMENT : VR_YAW_INCREMENT);
|
||||
yawTimer = CAMERA_UPDATE_TIME;
|
||||
} else if (!HMD.active) {
|
||||
yawChange = yawChange + (shifted ? SHIFT_MAG * YAW_INCREMENT : YAW_INCREMENT);
|
||||
}
|
||||
break;
|
||||
case findAction("YAW_RIGHT"):
|
||||
if (yawTimer < 0.0 && HMD.active) {
|
||||
yawChange = yawChange - (shifted ? SHIFT_MAG * VR_YAW_INCREMENT : VR_YAW_INCREMENT);
|
||||
yawTimer = CAMERA_UPDATE_TIME;
|
||||
} else if (!HMD.active) {
|
||||
yawChange = yawChange - (shifted ? SHIFT_MAG * YAW_INCREMENT : YAW_INCREMENT);
|
||||
}
|
||||
break;
|
||||
case findAction("PITCH_DOWN"):
|
||||
if (!HMD.active) {
|
||||
pitchChange = pitchChange - (shifted ? SHIFT_MAG * PITCH_INCREMENT : PITCH_INCREMENT);
|
||||
}
|
||||
break;
|
||||
case findAction("PITCH_UP"):
|
||||
if (!HMD.active) {
|
||||
pitchChange = pitchChange + (shifted ? SHIFT_MAG * PITCH_INCREMENT : PITCH_INCREMENT);
|
||||
}
|
||||
break;
|
||||
case findAction("SHIFT"): // speed up
|
||||
if (shiftTimer < 0.0) {
|
||||
shifted = !shifted;
|
||||
shiftTimer = SHIFT_UPDATE_TIME;
|
||||
}
|
||||
break;
|
||||
case findAction("ACTION1"): // start/end warp
|
||||
if (warpTimer < 0.0) {
|
||||
warpActive = !warpActive;
|
||||
if (!warpActive) {
|
||||
finishWarp();
|
||||
}
|
||||
warpTimer = WARP_UPDATE_TIME;
|
||||
}
|
||||
break;
|
||||
case findAction("ACTION2"): // cancel warp
|
||||
warpActive = false;
|
||||
Overlays.editOverlay(warpSphere, {
|
||||
visible: false,
|
||||
});
|
||||
Overlays.editOverlay(warpLine, {
|
||||
visible: false,
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function update(dt) {
|
||||
if (prevVRMode != HMD.active) {
|
||||
active = HMD.active;
|
||||
prevVRMode = HMD.active;
|
||||
}
|
||||
|
||||
if (yawTimer >= 0.0) {
|
||||
yawTimer = yawTimer - dt;
|
||||
}
|
||||
if (shiftTimer >= 0.0) {
|
||||
shiftTimer = shiftTimer - dt;
|
||||
}
|
||||
if (warpTimer >= 0.0) {
|
||||
warpTimer = warpTimer - dt;
|
||||
}
|
||||
|
||||
if (warpActive) {
|
||||
updateWarp();
|
||||
}
|
||||
|
||||
if (active) {
|
||||
Controller.captureActionEvents();
|
||||
|
||||
MyAvatar.bodyYaw = MyAvatar.bodyYaw + yawChange;
|
||||
MyAvatar.headPitch = Math.max(-180, Math.min(180, MyAvatar.headPitch + pitchChange));
|
||||
yawChange = 0;
|
||||
pitchChange = 0;
|
||||
|
||||
MyAvatar.motorVelocity = velocity;
|
||||
MyAvatar.motorTimescale = 0.0;
|
||||
velocity = { x: 0, y: 0, z: 0 };
|
||||
} else {
|
||||
Controller.releaseActionEvents();
|
||||
yawChange = 0;
|
||||
pitchChange = 0;
|
||||
MyAvatar.motorVelocity = {x:0.0, y:0.0, z:0.0}
|
||||
MyAvatar.motorTimescale = VERY_LONG_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
function updateWarp() {
|
||||
var look = Quat.getFront(Camera.getOrientation());
|
||||
var pitch = Math.asin(look.y);
|
||||
|
||||
// Get relative to looking straight down
|
||||
pitch += Math.PI / 2;
|
||||
|
||||
// Scale up
|
||||
pitch *= 2;
|
||||
var distance = pitch * pitch * pitch;
|
||||
|
||||
var warpDirection = Vec3.normalize({ x: look.x, y: 0, z: look.z });
|
||||
warpPosition = Vec3.multiply(warpDirection, distance);
|
||||
warpPosition = Vec3.sum(MyAvatar.position, warpPosition);
|
||||
|
||||
// Commented out until ray picking can be fixed
|
||||
// var pickRay = {
|
||||
// origin: Vec3.sum(warpPosition, WARP_PICK_OFFSET),
|
||||
// direction: { x: 0, y: -1, z: 0 }
|
||||
// };
|
||||
|
||||
// var intersection = Entities.findRayIntersection(pickRay);
|
||||
|
||||
// if (intersection.intersects && intersection.distance < WARP_PICK_MAX_DISTANCE) {
|
||||
// // Warp 1 meter above the object - this is an approximation
|
||||
// // TODO Get the actual offset to the Avatar's feet and plant them to
|
||||
// // the object.
|
||||
// warpPosition = Vec3.sum(intersection.intersection, { x: 0, y: 1, z:0 });
|
||||
// }
|
||||
|
||||
// Adjust overlays to match warp position
|
||||
Overlays.editOverlay(warpSphere, {
|
||||
position: warpPosition,
|
||||
visible: true,
|
||||
});
|
||||
Overlays.editOverlay(warpLine, {
|
||||
start: warpPosition,
|
||||
end: Vec3.sum(warpPosition, { x: 0, y: WARP_LINE_HEIGHT, z: 0 }),
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
|
||||
function finishWarp() {
|
||||
Overlays.editOverlay(warpSphere, {
|
||||
visible: false,
|
||||
});
|
||||
Overlays.editOverlay(warpLine, {
|
||||
visible: false,
|
||||
});
|
||||
MyAvatar.position = warpPosition;
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
Controller.keyPressEvent.connect(onKeyPressEvent);
|
||||
|
||||
Controller.actionEvent.connect(onActionEvent);
|
||||
|
||||
Script.update.connect(update);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
Controller.releaseActionEvents();
|
||||
MyAvatar.motorVelocity = {x:0.0, y:0.0, z:0.0}
|
||||
MyAvatar.motorTimescale = VERY_LONG_TIME;
|
||||
}
|
||||
|
||||
setUp();
|
||||
Script.scriptEnding.connect(tearDown);
|
||||
}());
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// hmdDefaults.js
|
||||
// examples
|
||||
//
|
||||
// Created by David Rowe on 6 Mar 2015.
|
||||
// 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
|
||||
//
|
||||
|
||||
Script.load("progress.js");
|
||||
Script.load("lobby.js");
|
||||
Script.load("notifications.js");
|
||||
Script.load("controllers/oculus/goTo.js");
|
||||
Script.load("hmdControls.js");
|
||||
//Script.load("scripts.js"); // Not created yet
|
109
examples/homeContent/whiteboardV2/eraserEntityScript.js
Normal file
109
examples/homeContent/whiteboardV2/eraserEntityScript.js
Normal 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();
|
||||
});
|
219
examples/homeContent/whiteboardV2/markerEntityScript.js
Normal file
219
examples/homeContent/whiteboardV2/markerEntityScript.js
Normal 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();
|
||||
});
|
254
examples/homeContent/whiteboardV2/whiteboardSpawner.js
Normal file
254
examples/homeContent/whiteboardV2/whiteboardSpawner.js
Normal 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);
|
883
examples/html/edit-style.css
Normal file
883
examples/html/edit-style.css
Normal file
|
@ -0,0 +1,883 @@
|
|||
/*
|
||||
// edit-style.css
|
||||
//
|
||||
// Created by Ryan Huffman on 13 Nov 2014
|
||||
// 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
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: Raleway-Regular;
|
||||
src: url(../../resources/fonts/Raleway-Regular.ttf), /* Production */
|
||||
url(../../interface/resources/fonts/Raleway-Regular.ttf); /* Development */
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Raleway-Light;
|
||||
src: url(../../resources/fonts/Raleway-Light.ttf),
|
||||
url(../../interface/resources/fonts/Raleway-Light.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Raleway-Bold;
|
||||
src: url(../../resources/fonts/Raleway-Bold.ttf),
|
||||
url(../../interface/resources/fonts/Raleway-Bold.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Raleway-SemiBold;
|
||||
src: url(../../resources/fonts/Raleway-SemiBold.ttf),
|
||||
url(../../interface/resources/fonts/Raleway-SemiBold.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: FiraSans-SemiBold;
|
||||
src: url(../../resources/fonts/FiraSans-SemiBold.ttf),
|
||||
url(../../interface/resources/fonts/FiraSans-SemiBold.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: AnonymousPro-Regular;
|
||||
src: url(../../resources/fonts/AnonymousPro-Regular.ttf),
|
||||
url(../../interface/resources/fonts/AnonymousPro-Regular.ttf);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: HiFi-Glyphs;
|
||||
src: url(../../resources/fonts/hifi-glyphs.ttf),
|
||||
url(../../interface/resources/fonts/hifi-glyphs.ttf);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 24px 12px 24px 12px;
|
||||
|
||||
color: #afafaf;
|
||||
background-color: #404040;
|
||||
font-family: Raleway-Regular;
|
||||
font-size: 15px;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 15px;
|
||||
color: #afafaf;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
border: 2px solid #575757;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
thead {
|
||||
font-family: Raleway-Regular;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
background-color: #1c1c1c;
|
||||
padding: 1px 0px;
|
||||
border-bottom: 1px solid #575757;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
font-family: Raleway-Light;
|
||||
font-size: 13px;
|
||||
background-color: #1c1c1c;
|
||||
border-top: 1px solid #575757;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tfoot tr {
|
||||
background-color: #1c1cff;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
height: 26px; /* 28px with thead padding */
|
||||
}
|
||||
|
||||
thead th {
|
||||
height: 26px;
|
||||
background-color: #1c1c1c;
|
||||
border-right: 1px solid #575757;
|
||||
}
|
||||
|
||||
thead th:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
tfoot td {
|
||||
height: 18px;
|
||||
width: 100%;
|
||||
background-color: #1c1c1c;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
tr {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr:nth-child(odd) {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
tr:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
tr.selected {
|
||||
color: #000000;
|
||||
background-color: #00b4ef;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: center;
|
||||
word-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-wrap: nowrap;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
td.url {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
input[type="text"], input[type="number"], textarea {
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
color: #afafaf;
|
||||
background-color: #252525;
|
||||
border: none;
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: AnonymousPro-Regular;
|
||||
font-size: 16px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
min-height: 64px;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
outline: 1px solid #00b4ef;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
input::selection, textarea::selection {
|
||||
color: #000000;
|
||||
background-color: #00b4ef;
|
||||
}
|
||||
|
||||
input.search {
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
input:disabled, textarea:disabled {
|
||||
background-color: #383838;
|
||||
color: #afafaf;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
height: 28px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
position: relative;
|
||||
height: 28px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
padding-right: 6px;
|
||||
}
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
opacity: 1.0;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 10px;
|
||||
overflow: hidden;
|
||||
font-family: hifi-glyphs;
|
||||
font-size: 50px;
|
||||
color: #afafaf;
|
||||
cursor: pointer;
|
||||
/*background-color: #000000;*/
|
||||
}
|
||||
input[type=number]::-webkit-inner-spin-button:before,
|
||||
input[type=number]::-webkit-inner-spin-button:after {
|
||||
position:absolute;
|
||||
left: -21px;
|
||||
line-height: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
input[type=number]::-webkit-inner-spin-button:before {
|
||||
content: "6";
|
||||
top: 5px;
|
||||
}
|
||||
input[type=number]::-webkit-inner-spin-button:after {
|
||||
content: "5";
|
||||
bottom: 6px;
|
||||
}
|
||||
input[type="number"]::-webkit-inner-spin-button:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
input.no-spin::-webkit-outer-spin-button,
|
||||
input.no-spin::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
input[type=button] {
|
||||
font-family: Raleway-Bold;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
vertical-align: top;
|
||||
height: 28px;
|
||||
min-width: 120px;
|
||||
padding: 0px 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
background: linear-gradient(#343434 20%, #000 100%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=button].glyph {
|
||||
font-family: HiFi-Glyphs;
|
||||
font-size: 20px;
|
||||
text-transform: none;
|
||||
min-width: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type=button].red {
|
||||
color: #fff;
|
||||
background-color: #94132e;
|
||||
background: linear-gradient(#d42043 20%, #94132e 100%);
|
||||
}
|
||||
input[type=button].blue {
|
||||
color: #fff;
|
||||
background-color: #94132e;
|
||||
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
|
||||
}
|
||||
|
||||
input[type=button]:enabled:hover {
|
||||
background: linear-gradient(#000, #000);
|
||||
border: none;
|
||||
}
|
||||
input[type=button].red:enabled:hover {
|
||||
background: linear-gradient(#d42043, #d42043);
|
||||
border: none;
|
||||
}
|
||||
input[type=button].blue:enabled:hover {
|
||||
background: linear-gradient(#00b4ef, #00b4ef);
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type=button]:active {
|
||||
background: linear-gradient(#343434, #343434);
|
||||
}
|
||||
input[type=button].red:active {
|
||||
background: linear-gradient(#94132e, #94132e);
|
||||
}
|
||||
input[type=button].blue:active {
|
||||
background: linear-gradient(#1080b8, #1080b8);
|
||||
}
|
||||
|
||||
input[type=button]:disabled {
|
||||
color: #252525;
|
||||
background: linear-gradient(#575757 20%, #252525 100%);
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
input[type=checkbox] + label {
|
||||
padding-left: 24px;
|
||||
background-position-y: 6px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACpSURBVDhPY2xoaGD68+dPMSMjY9L////VgTQjAw4AlH8PxLOPHj1azWxjY1MBVNsBFBfBpwkEgNKcQGwtJyfHyATkF0KEiQdAzYlMQEIUyicFyDD9+/ePgRxMvsb///4zkIOZ/v0HmkAGHginYjGNGAzS+BpdkAj8mun/3//92DyPD//993cG88nTJ4+Zm5p/BSZeJYb/DEJADEzNOPF7hn8Mk69cvVIPAHN5pyfo70F5AAAAAElFTkSuQmCC);
|
||||
}
|
||||
input[type=checkbox]:enabled + label:hover {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAClSURBVDhPY2hoaGD6//9/6b9//64D8T8gGycASr/7+/dv5/79+1kYgIxKqDjRAKiniRFIv2JgYBAFYlLAE0aQ66AckgDjjx8/yNP44cMH8jS+fPmSPI0PHz4kT+PNmzfJ03jp0iXyNJ46dYo8jYcPHyYnAbxm+vnzZz8wLhlIwd+/f5/BrKSkdExCQuLrnz9/lIBpUAiIQekXF34PTGmTT548WQ8AokXg+rhVtPYAAAAASUVORK5CYII=);
|
||||
}
|
||||
input[type=checkbox]:checked + label {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFLSURBVDhPjZK9SgNBFIXvbCaQRDQq7mIhQRPBRisJKcwLWOobaCsExEaxcEEQe0trGysfwg0EwWoDsbFIJUaIBJOwus547saNP3FlPzgzzJxzL5edFbZtG77v7wkhtrXWS9gFRQC/DZ07jnOYKJfL+8ie4n7mvyIGdhpay+VyQuK8y5dPZoHuVtbpZcLi4wjJ1x4t316R9dDgBlsSi8mGu7pJjyJFzVaH+r7iqyHSELSQzVADjS0UgjlDKUUsLzVO98+9kSLGV5qaHXhjU0GWNSxk3hCIwnsfeMNCjTArLmHeUBodoLiE+R+jxuHPUZP4elGE3teonx2S/Q7lJzOUlkYQ+A4/xzyegzNhXmJpwTMXry9IFjcoa84O0r+QXpcK1cugCLREZadyoA19Ergxwf96nKjd1KqlYqmLQ540TUNwItUmRWdu3T36AODjwgpY9xqqAAAAAElFTkSuQmCC);
|
||||
}
|
||||
input[type=checkbox]:checked + label:hover {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVDhPnZLPSsNAEMa/XVPBCE0RhNy0OarP4Av4AD6JB0GwVBA8efBBxHsgh4CQswcRoUIpiIpVAm3zZ5M4szFSbQPBH3xkJvNNZskOer2eLIriKM/ze1JOcS1UHmdZduF5ngEKjr/fN4Z6+oKerwA2gxC4HAFPEWVLsAzgZAvYt3Q6Enw6jg7uBAaTFMNwhpnKdbXCkAJdy8ROu4XrXW2HTJIErHcFDD6nC02Mom8PwymeE2gvS0ZRBBaTlsOXEmdlrfLLOI7Bakrl/zWxCT8T/904f9QW/b06qtrCUdtFCqdjYs2Q2jAPX8c2XQd7Kr/wfV8vwIPs4Ga1ixe5Xrr/YFLTYfKIvWzM6ZtwXZdX7lxXG0L+sxXHcW5t254opRzawQ0S72+dPmjTroIgOP0CQSMt5LDn1T8AAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
.selectable {
|
||||
-webkit-touch-callout: text;
|
||||
-webkit-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.color-box {
|
||||
display: inline-block;
|
||||
width: 15pt;
|
||||
height: 15pt;
|
||||
border: 0.75pt solid black;
|
||||
margin: 1.5pt;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-box.highlight {
|
||||
width: 13.5pt;
|
||||
height: 13.5pt;
|
||||
border: 1.5pt solid black;
|
||||
}
|
||||
|
||||
|
||||
.section-header, .sub-section-header {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin: 22px -12px 0 -12px;
|
||||
padding: 14px 12px 0 12px;
|
||||
font-family: Raleway-Regular;
|
||||
font-size: 12px;
|
||||
color: #afafaf;
|
||||
height: 28px;
|
||||
text-transform: uppercase;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
position: relative;
|
||||
background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAqCAIAAAAbNW1vAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAmSURBVChTY1BFAgzhSIDBAQmMcoYHRwIJMCgjAQZ9JMBgBQdWVgBh5XmBV5A2FQAAAABJRU5ErkJggg==) repeat-x top left;
|
||||
}
|
||||
|
||||
.sub-section-header, .no-collapse {
|
||||
background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left;
|
||||
}
|
||||
|
||||
.section-header:first-child {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
background: none;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.sub-section-header {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.section-header span {
|
||||
font-family: HiFi-Glyphs;
|
||||
font-size: 30px;
|
||||
float: right;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.section-header[collapsed="true"] {
|
||||
margin-bottom: -22px;
|
||||
}
|
||||
|
||||
.text-group[collapsed="true"] ~ .text-group,
|
||||
.zone-group[collapsed="true"] ~ .zone-group,
|
||||
.web-group[collapsed="true"] ~ .web-group,
|
||||
.hyperlink-group[collapsed="true"] ~ .hyperlink-group,
|
||||
.spatial-group[collapsed="true"] ~ .spatial-group,
|
||||
.physical-group[collapsed="true"] ~ .physical-group,
|
||||
.behavior-group[collapsed="true"] ~ .behavior-group,
|
||||
.model-group[collapsed="true"] ~ .model-group,
|
||||
.light-group[collapsed="true"] ~ .light-group {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
.property {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: 22px;
|
||||
min-height: 29px;
|
||||
}
|
||||
|
||||
.property label {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
font-family: Raleway-SemiBold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: block;
|
||||
min-height: 18px;
|
||||
}
|
||||
.value label {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 48px;
|
||||
}
|
||||
.value span {
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.checkbox + .checkbox {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.checkbox-sub-props {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.property .number {
|
||||
float: left;
|
||||
}
|
||||
.property .number + .number {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.text label, .url label, .number label, .textarea label, .rgb label, .xyz label, .pyr label, .dropdown label, .gen label {
|
||||
float: left;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.number > input {
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
.number > span {
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
.xyz > div, .pyr > div, .gen > div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.unit {
|
||||
padding-left: 4px;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
margin-bottom: -17px;
|
||||
}
|
||||
|
||||
.dropdown select {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.dropdown dl {
|
||||
clear: both;
|
||||
}
|
||||
.dropdown dl {
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 15px;
|
||||
width: 172px;
|
||||
height: 28px;
|
||||
padding: 0 28px 0 12px;
|
||||
|
||||
color: #afafaf;
|
||||
background: linear-gradient(#7d7d7d 20%, #686a68 100%);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
.dropdown dl[dropped="true"] {
|
||||
color: #404040;
|
||||
background: linear-gradient(#afafaf, #afafaf);
|
||||
}
|
||||
|
||||
.dropdown dt {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #121212;
|
||||
width: 100%;
|
||||
}
|
||||
.dropdown dt:hover {
|
||||
color: #404040;
|
||||
}
|
||||
.dropdown dt:focus {
|
||||
outline: none;
|
||||
}
|
||||
.dropdown dt span:first-child {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
}
|
||||
.dropdown dt span:last-child {
|
||||
font-family: HiFi-Glyphs;
|
||||
font-size: 42px;
|
||||
float: right;
|
||||
margin-right: -48px;
|
||||
position: relative;
|
||||
left: -12px;
|
||||
top: -11px;
|
||||
}
|
||||
|
||||
.dropdown dd {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 3px;
|
||||
display: none;
|
||||
}
|
||||
.dropdown dl[dropped="true"] dd {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown li {
|
||||
list-style-type: none;
|
||||
padding: 3px 0 1px 12px;
|
||||
width: 200px;
|
||||
height: auto;
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 15px;
|
||||
color: #404040;
|
||||
background-color: #afafaf
|
||||
}
|
||||
.dropdown li:hover {
|
||||
background-color: #00b4ef;
|
||||
}
|
||||
|
||||
|
||||
div.refresh {
|
||||
box-sizing: border-box;
|
||||
padding-right: 44px;
|
||||
}
|
||||
div.refresh input[type="button"] {
|
||||
float: right;
|
||||
margin-right: -44px;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
margin-bottom: 12px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid #afafaf;
|
||||
border-radius: 4px;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABTSURBVChTjcxBDoAwCETRwTs33sFjwB6uaCE1Ggvav5qQF7CSqu40dllHjYiOT3gh3yV8Ii+Fb+RNMEP9hm3sKENmBhG5P1aImWMH/EMerSAAOAFgTC/R8ZXSXAAAAABJRU5ErkJggg==);
|
||||
background-position: bottom right;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.color-picker:focus {
|
||||
outline: none;
|
||||
}
|
||||
.color-picker[active="true"] {
|
||||
border-color: #000;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABDSURBVChTjcoLCkAhCETRNq0tf97Y5xGZ1gVJ45TH6njThIO+xk2UwhWFcEdH6JCqOuiQiMDi/hcii3crRRb/7ggAPvIMVihQwvSXAAAAAElFTkSuQmCC);
|
||||
}
|
||||
|
||||
.rgb label {
|
||||
float: left;
|
||||
margin-top: 10px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.rgb label + * {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.tuple {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.tuple div {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
min-width: 120px;
|
||||
min-height: 1px;
|
||||
}
|
||||
.tuple div:nth-child(1) {
|
||||
float: left;
|
||||
}
|
||||
.tuple div:nth-child(2) {
|
||||
}
|
||||
.tuple div:nth-child(3) {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.rgb .tuple input {
|
||||
padding-left: 65px;
|
||||
}
|
||||
.xyz .tuple input {
|
||||
padding-left: 25px;
|
||||
}
|
||||
.pyr .tuple input {
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
.tuple div > label:first-child {
|
||||
float: left;
|
||||
}
|
||||
.tuple div > label + input {
|
||||
clear: both;
|
||||
float: left;
|
||||
}
|
||||
.tuple div input + label {
|
||||
display: inline !important;
|
||||
float: none !important;
|
||||
position: absolute;
|
||||
margin-top: 8px;
|
||||
margin-left: 6px;
|
||||
left: 0;
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 12px;
|
||||
}
|
||||
.tuple .red + label, .tuple .x + label, .tuple .pitch + label {
|
||||
color: #e2334d;
|
||||
}
|
||||
.tuple .green + label, .tuple .y + label, .tuple .yaw + label {
|
||||
color: #1ac567;
|
||||
}
|
||||
.tuple .blue + label, .tuple .z + label, .tuple .roll + label {
|
||||
color: #1080b8;
|
||||
}
|
||||
|
||||
.tuple .red:focus, .tuple .x:focus, .tuple .pitch:focus {
|
||||
outline-color: #e2334d;
|
||||
}
|
||||
.tuple .green:focus, .tuple .y:focus, .tuple .yaw:focus {
|
||||
outline-color: #1ac567;
|
||||
}
|
||||
tuple, .blue:focus, .tuple .z:focus, .tuple .roll:focus {
|
||||
outline-color: #1080b8;
|
||||
}
|
||||
|
||||
.xyz .buttons input {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.xyz .buttons span {
|
||||
word-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row input {
|
||||
float: left;
|
||||
}
|
||||
.row input[type=button] {
|
||||
margin-left: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #696969;
|
||||
border: 2px solid #2e2e2e;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* FIXME: Revisit textarea resizer/corner when move to Qt 5.6 or later: see if can get resizer/corner to always be visible and
|
||||
have correct background color with and without scrollbars. */
|
||||
textarea:enabled::-webkit-resizer {
|
||||
background-size: 10px 10px;
|
||||
background: #252525 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAXSURBVChTY1RVVWXADZigNA4wMqUZGACS3gCD5UUtKAAAAABJRU5ErkJggg==) no-repeat bottom right;
|
||||
}
|
||||
textarea:focus::-webkit-resizer {
|
||||
background-size: 10px 10px;
|
||||
background: #000000 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACOSURBVChThdC5DQQhDAXQvyRI5LQxFdABARWQElAPogYkiqEWQhLYGe8xxzJaS5a/8AuQHwDG2n+Lvee0hBDQWlO+hRvy3mNZFjDG5vCDOOeIMaL3/guPKISAWiu9n+AVSSlhraXdF86Qcw6tNdoTvEOlFOScd6iUOv3JGEMopYQx9jNvaawnoHnNr8Z4AuRLPOq2gPgnAAAAAElFTkSuQmCC) no-repeat bottom right;
|
||||
}
|
||||
textarea:enabled[scrolling="true"]::-webkit-resizer {
|
||||
background-size: 10px 10px;
|
||||
background: #2e2e2e url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACKSURBVChTjdAxDsMgDAXQT4UYuQIzCwsSKxsSJ4YDoByDY7AwUOG2aZMQqX+xhd9gzIwxA3/k8a7LCCFgraX+Fk4UY4RSCoyxNfwgzjlyzhhjXOEvSimhtUbvB3hGUkp472m2wxUKIaD3TnOCd6jWim3bvlBrfdjJOUeolEJoZj/4PMH83bl/BXgCWSs2Z09IjgoAAAAASUVORK5CYII=) no-repeat bottom right;
|
||||
}
|
||||
|
||||
|
||||
#entity-list-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#delete {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
background-color: #ff0000;
|
||||
}
|
||||
|
||||
#entity-list {
|
||||
position: relative; /* New positioning context. */
|
||||
}
|
||||
|
||||
#search-area {
|
||||
padding-right: 148px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
#filter {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
#radius-and-unit {
|
||||
float: right;
|
||||
margin-right: -148px;
|
||||
}
|
||||
|
||||
|
||||
#entity-table-scroll {
|
||||
/* Height is set by JavaScript. */
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-top: 28px; /* Space for header and footer outside of scroll region. */
|
||||
margin-top: 28px;
|
||||
border-left: 2px solid #575757;
|
||||
border-right: 2px solid #575757;
|
||||
}
|
||||
|
||||
#entity-table-scroll, #entity-table {
|
||||
background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
#entity-table {
|
||||
margin-top: -28px;
|
||||
margin-bottom: -18px;
|
||||
table-layout: fixed;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#entity-table thead {
|
||||
border: 2px solid #575757;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom: 1px solid #575757;
|
||||
}
|
||||
|
||||
#entity-table tfoot {
|
||||
border: 2px solid #575757;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
border-top: 1px solid #575757;
|
||||
}
|
||||
|
||||
#entity-table thead tr, #entity-table thead tr th,
|
||||
#entity-table tfoot tr, #entity-table tfoot tr td {
|
||||
background: none;
|
||||
}
|
||||
|
||||
#entity-table th:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#col-type {
|
||||
width: 16%;
|
||||
}
|
||||
#col-name {
|
||||
width: 42%;
|
||||
}
|
||||
#col-url {
|
||||
width: 42%;
|
||||
}
|
||||
|
||||
#entity-table thead {
|
||||
position: absolute;
|
||||
top: 49px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#entity-table thead th {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#entity-table tfoot {
|
||||
position: absolute;
|
||||
bottom: -21px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#no-entities {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
padding: 12px;
|
||||
font-family: FiraSans-SemiBold;
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
color: #afafaf;
|
||||
}
|
||||
|
||||
|
||||
#properties-list .property:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#property-id::selection {
|
||||
color: #000000;
|
||||
background-color: #00b4ef;
|
||||
}
|
||||
|
||||
|
||||
input#dimension-rescale-button {
|
||||
min-width: 50px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
|
||||
.color-set label, .color-set span {
|
||||
display: block;
|
||||
}
|
||||
.color-set span {
|
||||
padding-top: 2px;
|
||||
}
|
|
@ -1,6 +1,16 @@
|
|||
<!--
|
||||
// entityList.html
|
||||
//
|
||||
// Created by Ryan Huffman on 19 Nov 2014
|
||||
// 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
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="stylesheet" type="text/css" href="edit-style.css">
|
||||
<script src="list.min.js"></script>
|
||||
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="text/javascript" src="eventBridgeLoader.js"></script>
|
||||
|
@ -26,8 +36,10 @@
|
|||
elDelete = document.getElementById("delete");
|
||||
elTeleport = document.getElementById("teleport");
|
||||
elRadius = document.getElementById("radius");
|
||||
elFooter = document.getElementById("footer-text");
|
||||
elNoEntitiesMessage = document.getElementById("no-entities");
|
||||
elNoEntitiesRadius = document.getElementById("no-entities-radius");
|
||||
elEntityTableScroll = document.getElementById("entity-table-scroll");
|
||||
|
||||
document.getElementById("entity-name").onclick = function() {
|
||||
setSortColumn('name');
|
||||
|
@ -168,6 +180,17 @@
|
|||
notFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedEntities.length > 1) {
|
||||
elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected";
|
||||
} else if (selectedEntities.length === 1) {
|
||||
elFooter.firstChild.nodeValue = "1 entity selected";
|
||||
} else if (entityList.visibleItems.length === 1) {
|
||||
elFooter.firstChild.nodeValue = "1 entity found";
|
||||
} else {
|
||||
elFooter.firstChild.nodeValue = entityList.visibleItems.length + " entities found";
|
||||
}
|
||||
|
||||
return notFound;
|
||||
}
|
||||
|
||||
|
@ -214,57 +237,90 @@
|
|||
} else if (data.type == "update") {
|
||||
var newEntities = data.entities;
|
||||
if (newEntities.length == 0) {
|
||||
elEntityTable.style.display = "none";
|
||||
elNoEntitiesMessage.style.display = "block";
|
||||
} else {
|
||||
elEntityTable.style.display = "table";
|
||||
elNoEntitiesMessage.style.display = "none";
|
||||
for (var i = 0; i < newEntities.length; i++) {
|
||||
var id = newEntities[i].id;
|
||||
addEntity(id, newEntities[i].name, newEntities[i].type, newEntities[i].url);
|
||||
}
|
||||
updateSelectedEntities(data.selectedIDs);
|
||||
resize();
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(refreshEntities, 1000);
|
||||
}
|
||||
|
||||
function resize() {
|
||||
// Take up available window space
|
||||
elEntityTableScroll.style.height = window.innerHeight - 232;
|
||||
|
||||
// Update the widths of the header cells to match the body
|
||||
var tds = document.querySelectorAll("#entity-table-body tr:first-child td");
|
||||
var ths = document.querySelectorAll("#entity-table thead th");
|
||||
if (tds.length >= ths.length) {
|
||||
for (var i = 0; i < ths.length; i++) {
|
||||
ths[i].style.width = tds[i].offsetWidth;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.onresize = resize;
|
||||
resize();
|
||||
});
|
||||
}
|
||||
|
||||
// Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
|
||||
document.addEventListener("contextmenu", function (event) {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload='loaded();'>
|
||||
<div id="entity-list-header">
|
||||
<input type="button" id="refresh" value="Refresh" />
|
||||
<input type="button" id="teleport" value="Teleport" />
|
||||
<input type="button" id="delete" style="background-color: rgb(244, 64, 64); float: right" value="Delete" />
|
||||
<input type="button" class="glyph" id="refresh" value="F" />
|
||||
<input type="button" id="teleport" value="Jump To Selection" />
|
||||
<input type="button" class="red" id="delete" value="Delete" />
|
||||
</div>
|
||||
|
||||
<div id="entity-list">
|
||||
<div id="search-area">
|
||||
<input type="text" class="search" id="filter" placeholder="Filter" />
|
||||
<span id="radius-and-unit"><input type="number" id="radius" value="100" /> m</span>
|
||||
<span id="radius-and-unit"><input type="number" id="radius" value="100" /><span class="unit">m</span></span>
|
||||
</div>
|
||||
<div id="entity-table-scroll">
|
||||
<table id="entity-table">
|
||||
<colgroup>
|
||||
<col span="1" id="col-type" />
|
||||
<col span="1" id="col-name" />
|
||||
<col span="1" id="col-url" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="entity-type" data-sort="type">Type <span class="sort-order" style="display: inline"> ▾</span></th>
|
||||
<th id="entity-name" data-sort="type">Name <span class="sort-order" style="display: none"> ▾</span></th>
|
||||
<th id="entity-url" data-sort="url">File <span class="sort-order" style="display: none"> ▾</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="entity-table-body">
|
||||
<tr>
|
||||
<td class="type">Type</td>
|
||||
<td class="name">Name</td>
|
||||
<td class="url"><div class='outer'><div class='inner'>URL</div></div></td>
|
||||
<td class="id" style="display: none">Type</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td id="footer-text" colspan="3">Footer text</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div id="no-entities">
|
||||
No entities found within a <span id="no-entities-radius">100</span> meter radius. Try moving to a different location and refreshing.
|
||||
</div>
|
||||
</div>
|
||||
<table id="entity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="entity-type" data-sort="type">Type <span class="sort-order" style="display: inline"> ▾</span></th>
|
||||
<th id="entity-name" data-sort="type">Name <span class="sort-order" style="display: inline"> ▾</span></th>
|
||||
<th id="entity-url" data-sort="url">URL <span class="sort-order" style="display: none"> ▾</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="entity-table-body">
|
||||
<tr>
|
||||
<td class="id" style="display: none">Type</td>
|
||||
<td class="type">Type</td>
|
||||
<td class="name">Name</td>
|
||||
<td class="url"><div class='outer'><div class='inner'>URL</div></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="no-entities">
|
||||
No entities found within a <span id="no-entities-radius">100</span> meter radius. Try moving to a different location and refreshing.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,51 +9,11 @@
|
|||
//
|
||||
|
||||
var EventBridge;
|
||||
|
||||
EventBridgeConnectionProxy = function(parent) {
|
||||
this.parent = parent;
|
||||
this.realSignal = this.parent.realBridge.scriptEventReceived
|
||||
this.webWindowId = this.parent.webWindow.windowId;
|
||||
}
|
||||
|
||||
EventBridgeConnectionProxy.prototype.connect = function(callback) {
|
||||
var that = this;
|
||||
this.realSignal.connect(function(id, message) {
|
||||
if (id === that.webWindowId) { callback(message); }
|
||||
});
|
||||
}
|
||||
|
||||
EventBridgeProxy = function(webWindow) {
|
||||
this.webWindow = webWindow;
|
||||
this.realBridge = this.webWindow.eventBridge;
|
||||
this.scriptEventReceived = new EventBridgeConnectionProxy(this);
|
||||
}
|
||||
|
||||
EventBridgeProxy.prototype.emitWebEvent = function(data) {
|
||||
this.realBridge.emitWebEvent(data);
|
||||
}
|
||||
var WebChannel;
|
||||
|
||||
openEventBridge = function(callback) {
|
||||
EVENT_BRIDGE_URI = "ws://localhost:51016";
|
||||
socket = new WebSocket(this.EVENT_BRIDGE_URI);
|
||||
|
||||
socket.onclose = function() {
|
||||
console.error("web channel closed");
|
||||
};
|
||||
|
||||
socket.onerror = function(error) {
|
||||
console.error("web channel error: " + error);
|
||||
};
|
||||
|
||||
socket.onopen = function() {
|
||||
channel = new QWebChannel(socket, function(channel) {
|
||||
console.log("Document url is " + document.URL);
|
||||
var webWindow = channel.objects[document.URL.toLowerCase()];
|
||||
console.log("WebWindow is " + webWindow)
|
||||
eventBridgeProxy = new EventBridgeProxy(webWindow);
|
||||
EventBridge = eventBridgeProxy;
|
||||
if (callback) { callback(eventBridgeProxy); }
|
||||
});
|
||||
}
|
||||
WebChannel = new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
EventBridge = WebChannel.objects.eventBridgeWrapper.eventBridge;
|
||||
callback(EventBridge);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
<!--
|
||||
// gridControls.html
|
||||
//
|
||||
// Created by Ryan Huffman on 6 Nov 2014
|
||||
// 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
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<link rel="stylesheet" type="text/css" href="edit-style.css">
|
||||
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="text/javascript" src="eventBridgeLoader.js"></script>
|
||||
<script>
|
||||
|
@ -33,8 +43,8 @@
|
|||
elPosY.value = origin.y.toFixed(2);
|
||||
}
|
||||
|
||||
if (data.minorGridWidth !== undefined) {
|
||||
elMinorSpacing.value = data.minorGridWidth;
|
||||
if (data.minorGridEvery !== undefined) {
|
||||
elMinorSpacing.value = data.minorGridEvery;
|
||||
}
|
||||
|
||||
if (data.majorGridEvery !== undefined) {
|
||||
|
@ -60,7 +70,7 @@
|
|||
origin: {
|
||||
y: elPosY.value,
|
||||
},
|
||||
minorGridWidth: elMinorSpacing.value,
|
||||
minorGridEvery: elMinorSpacing.value,
|
||||
majorGridEvery: elMajorSpacing.value,
|
||||
gridColor: gridColor,
|
||||
colorIndex: gridColorIndex,
|
||||
|
@ -109,61 +119,63 @@
|
|||
|
||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'init' }));
|
||||
});
|
||||
|
||||
// Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
|
||||
document.addEventListener("contextmenu", function (event) {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload='loaded();'>
|
||||
<div class="grid-section">
|
||||
<div id="grid-section">
|
||||
|
||||
<div class="property-section">
|
||||
<label>Visible</label>
|
||||
<div class="section-header">
|
||||
<label>Editing Grid</label>
|
||||
</div>
|
||||
|
||||
<div class="property checkbox">
|
||||
<input type='checkbox' id="horiz-grid-visible">
|
||||
<label for="horiz-grid-visible">Visible</label>
|
||||
</div>
|
||||
|
||||
<div class="property checkbox">
|
||||
<input type="checkbox" id="snap-to-grid">
|
||||
<label for="snap-to-grid">Snap entities to grid</label>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<div class="number">
|
||||
<label>Major grid size</label>
|
||||
<span>
|
||||
<input type="number" id="major-spacing" min="1" step="1" /><span class="unit">m</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="number">
|
||||
<label>Minor grid size</label>
|
||||
<span>
|
||||
<input type="number" id="minor-spacing" min="0.2" step="0.2" /><span class="unit">m</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property number">
|
||||
<label>Position (Y axis)</label>
|
||||
<span>
|
||||
<input type='checkbox' id="horiz-grid-visible">
|
||||
<input type="number" id="horiz-y" step="0.1" /><span class="unit">m</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<label>Snap to grid</label>
|
||||
<span>
|
||||
<input type='checkbox' id="snap-to-grid">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="horizontal-position" class="property-section">
|
||||
<label>Position (Y Axis)</label>
|
||||
<span>
|
||||
<input type='number' id="horiz-y" class="number" value="0.0" step="0.1"></input>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<label>Minor Grid Size</label>
|
||||
<span>
|
||||
<input type='number' id="minor-spacing" min="0" step="0.01", ></input>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<label>Major Grid Every</label>
|
||||
<span>
|
||||
<input type='number' id="major-spacing" min="2" step="1", ></input>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<label>Grid Color</label>
|
||||
<div class="property color-set">
|
||||
<label>Grid line color</label>
|
||||
<span id="grid-colors"></span>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<div class="property">
|
||||
<span>
|
||||
<input type="button" id="move-to-selection" value="Move to Selection">
|
||||
</span>
|
||||
</div>
|
||||
<div class="property-section">
|
||||
<span>
|
||||
<input type="button" id="move-to-avatar" value="Move to Avatar">
|
||||
</span>
|
||||
<input type="button" id="move-to-selection" value="Align To Selection">
|
||||
<input type="button" id="move-to-avatar" value="Align To Avatar">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -4,21 +4,17 @@
|
|||
<script type="text/javascript" src="jquery-2.1.4.min.js"></script>
|
||||
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
<script type="text/javascript" src="eventBridgeLoader.js"></script>
|
||||
|
||||
<script>
|
||||
var myBridge;
|
||||
|
||||
window.onload = function() {
|
||||
openEventBridge(function(eventBridge) {
|
||||
myBridge = eventBridge;
|
||||
myBridge.scriptEventReceived.connect(function(message) {
|
||||
openEventBridge(function() {
|
||||
EventBridge.scriptEventReceived.connect(function(message) {
|
||||
console.log("HTML side received message: " + message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testClick = function() {
|
||||
myBridge.emitWebEvent("HTML side sending message - button click");
|
||||
EventBridge.emitWebEvent(["Foo", "Bar", { "baz": 1} ]);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
|
|
@ -134,8 +134,18 @@ textarea {
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
.update-url-version{
|
||||
width:17px;
|
||||
height:17px;
|
||||
float:right;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAQAAACRZI9xAAABAUlEQVQoz33RvyvEARjH8SdlUX6UbhFJfgwmE3aTxWEiitxqN/gH7IxCKTcymLiJFIOJsihiUKcrCYnuXoZv+F45n2d4lvfz6fPpCSFs6BO1JllvLmXk5F37dC0vJ5NGmlBxoFp7Rn6RDpRRtG5KtynrijhLoBBGQcllKkFWAXsyCbKEFxeGqmJmFZFLkE639vX+12hCSVft0nUR8RbLcRN/aSHGIqL2tVYPconL32qLtaiL88Rl0qtXc+pTDv1OPGMlidtgS8Wp2RR05FEFM98/GlRQ9mTHvB7jVt2rKGPAT9xh2z6qfnToHV1SjZpN23Tl051di1oco9W/pdttaBRfEhFXOZV7vEsAAAAASUVORK5CYII=);
|
||||
padding:0 !important;
|
||||
margin:0 2px 0 0 !important;
|
||||
}
|
||||
|
||||
input.url {
|
||||
width: 100%;
|
||||
width:85%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
input.coord {
|
||||
|
|
|
@ -5,7 +5,7 @@ EntityListTool = function(opts) {
|
|||
|
||||
var url = ENTITY_LIST_HTML_URL;
|
||||
var webView = new OverlayWebWindow({
|
||||
title: 'Entities', source: url, toolWindow: true
|
||||
title: 'Entity List', source: url, toolWindow: true
|
||||
});
|
||||
|
||||
|
||||
|
@ -38,14 +38,14 @@ EntityListTool = function(opts) {
|
|||
type: 'selectionUpdate',
|
||||
selectedIDs: selectedIDs,
|
||||
};
|
||||
webView.eventBridge.emitScriptEvent(JSON.stringify(data));
|
||||
webView.emitScriptEvent(JSON.stringify(data));
|
||||
});
|
||||
|
||||
that.clearEntityList = function () {
|
||||
var data = {
|
||||
type: 'clearEntityList'
|
||||
}
|
||||
webView.eventBridge.emitScriptEvent(JSON.stringify(data));
|
||||
webView.emitScriptEvent(JSON.stringify(data));
|
||||
};
|
||||
|
||||
that.sendUpdate = function() {
|
||||
|
@ -72,11 +72,11 @@ EntityListTool = function(opts) {
|
|||
entities: entities,
|
||||
selectedIDs: selectedIDs,
|
||||
};
|
||||
webView.eventBridge.emitScriptEvent(JSON.stringify(data));
|
||||
webView.emitScriptEvent(JSON.stringify(data));
|
||||
}
|
||||
|
||||
|
||||
webView.eventBridge.webEventReceived.connect(function(data) {
|
||||
webView.webEventReceived.connect(function(data) {
|
||||
data = JSON.parse(data);
|
||||
if (data.type == "selectionUpdate") {
|
||||
var ids = data.entityIds;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -4253,16 +4316,23 @@ SelectionDisplay = (function() {
|
|||
return false;
|
||||
};
|
||||
|
||||
|
||||
that.updateHandleSizes = function() {
|
||||
if (selectionManager.hasSelection()) {
|
||||
var diff = Vec3.subtract(selectionManager.worldPosition, Camera.getPosition());
|
||||
var grabberSize = Vec3.length(diff) * GRABBER_DISTANCE_TO_SIZE_RATIO;
|
||||
var dimensions = SelectionManager.worldDimensions;
|
||||
var avgDimension = (dimensions.x + dimensions.y + dimensions.z) / 3;
|
||||
grabberSize = Math.min(grabberSize, avgDimension / 10);
|
||||
|
||||
for (var i = 0; i < stretchHandles.length; i++) {
|
||||
Overlays.editOverlay(stretchHandles[i], {
|
||||
size: grabberSize,
|
||||
});
|
||||
}
|
||||
var handleSize = Vec3.length(diff) * GRABBER_DISTANCE_TO_SIZE_RATIO * 10;
|
||||
var handleSize = Vec3.length(diff) * GRABBER_DISTANCE_TO_SIZE_RATIO * 7;
|
||||
handleSize = Math.min(handleSize, avgDimension / 3);
|
||||
|
||||
Overlays.editOverlay(yawHandle, {
|
||||
scale: handleSize,
|
||||
});
|
||||
|
@ -4279,7 +4349,7 @@ SelectionDisplay = (function() {
|
|||
});
|
||||
Overlays.editOverlay(grabberMoveUp, {
|
||||
position: pos,
|
||||
scale: handleSize / 2,
|
||||
scale: handleSize / 1.25,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,24 +12,26 @@ Grid = function(opts) {
|
|||
];
|
||||
var colorIndex = 0;
|
||||
var gridAlpha = 0.6;
|
||||
var origin = { x: 0, y: 0, z: 0 };
|
||||
var origin = { x: 0, y: +MyAvatar.getJointPosition('LeftToeBase').y.toFixed(1) + 0.1, z: 0 };
|
||||
var scale = 500;
|
||||
var minorGridEvery = 1.0;
|
||||
var majorGridEvery = 5;
|
||||
var minorGridWidth = 0.2;
|
||||
var halfSize = 40;
|
||||
var yOffset = 0.001;
|
||||
|
||||
var worldSize = 16384;
|
||||
|
||||
var snapToGrid = false;
|
||||
|
||||
var gridOverlay = Overlays.addOverlay("grid", {
|
||||
position: { x: 0 , y: 0, z: 0 },
|
||||
visible: true,
|
||||
rotation: Quat.fromPitchYawRollDegrees(90, 0, 0),
|
||||
dimensions: { x: scale, y: scale, z: scale },
|
||||
position: origin,
|
||||
visible: false,
|
||||
drawInFront: false,
|
||||
color: colors[0],
|
||||
alpha: gridAlpha,
|
||||
rotation: Quat.fromPitchYawRollDegrees(90, 0, 0),
|
||||
minorGridWidth: 0.1,
|
||||
majorGridEvery: 2,
|
||||
minorGridEvery: minorGridEvery,
|
||||
majorGridEvery: majorGridEvery,
|
||||
});
|
||||
|
||||
that.visible = false;
|
||||
|
@ -39,17 +41,13 @@ Grid = function(opts) {
|
|||
return origin;
|
||||
}
|
||||
|
||||
that.getMinorIncrement = function() { return minorGridWidth; };
|
||||
that.getMajorIncrement = function() { return minorGridWidth * majorGridEvery; };
|
||||
|
||||
that.getMinorGridWidth = function() { return minorGridWidth; };
|
||||
that.setMinorGridWidth = function(value) {
|
||||
minorGridWidth = value;
|
||||
that.getMinorIncrement = function() { return minorGridEvery; };
|
||||
that.setMinorIncrement = function(value) {
|
||||
minorGridEvery = value;
|
||||
updateGrid();
|
||||
};
|
||||
|
||||
that.getMajorGridEvery = function() { return majorGridEvery; };
|
||||
that.setMajorGridEvery = function(value) {
|
||||
}
|
||||
that.getMajorIncrement = function() { return majorGridEvery; };
|
||||
that.setMajorIncrement = function(value) {
|
||||
majorGridEvery = value;
|
||||
updateGrid();
|
||||
};
|
||||
|
@ -106,7 +104,7 @@ Grid = function(opts) {
|
|||
dimensions = { x: 0, y: 0, z: 0 };
|
||||
}
|
||||
|
||||
var spacing = majorOnly ? (minorGridWidth * majorGridEvery) : minorGridWidth;
|
||||
var spacing = majorOnly ? majorGridEvery : minorGridEvery;
|
||||
|
||||
position = Vec3.subtract(position, origin);
|
||||
|
||||
|
@ -122,7 +120,7 @@ Grid = function(opts) {
|
|||
return delta;
|
||||
}
|
||||
|
||||
var spacing = majorOnly ? (minorGridWidth * majorGridEvery) : minorGridWidth;
|
||||
var spacing = majorOnly ? majorGridEvery : minorGridEvery;
|
||||
|
||||
var snappedDelta = {
|
||||
x: Math.round(delta.x / spacing) * spacing,
|
||||
|
@ -135,9 +133,7 @@ Grid = function(opts) {
|
|||
|
||||
|
||||
that.setPosition = function(newPosition, noUpdate) {
|
||||
origin = Vec3.subtract(newPosition, { x: 0, y: yOffset, z: 0 });
|
||||
origin.x = 0;
|
||||
origin.z = 0;
|
||||
origin = { x: 0, y: newPosition.y, z: 0 };
|
||||
updateGrid();
|
||||
|
||||
if (!noUpdate) {
|
||||
|
@ -149,7 +145,7 @@ Grid = function(opts) {
|
|||
if (that.onUpdate) {
|
||||
that.onUpdate({
|
||||
origin: origin,
|
||||
minorGridWidth: minorGridWidth,
|
||||
minorGridEvery: minorGridEvery,
|
||||
majorGridEvery: majorGridEvery,
|
||||
gridSize: halfSize,
|
||||
visible: that.visible,
|
||||
|
@ -171,8 +167,8 @@ Grid = function(opts) {
|
|||
that.setPosition(pos, true);
|
||||
}
|
||||
|
||||
if (data.minorGridWidth) {
|
||||
minorGridWidth = data.minorGridWidth;
|
||||
if (data.minorGridEvery) {
|
||||
minorGridEvery = data.minorGridEvery;
|
||||
}
|
||||
|
||||
if (data.majorGridEvery) {
|
||||
|
@ -191,20 +187,22 @@ Grid = function(opts) {
|
|||
that.setVisible(data.visible, true);
|
||||
}
|
||||
|
||||
updateGrid();
|
||||
updateGrid(true);
|
||||
}
|
||||
|
||||
function updateGrid() {
|
||||
function updateGrid(noUpdate) {
|
||||
Overlays.editOverlay(gridOverlay, {
|
||||
position: { x: origin.y, y: origin.y, z: -origin.y },
|
||||
position: { x: 0, y: origin.y, z: 0 },
|
||||
visible: that.visible && that.enabled,
|
||||
minorGridWidth: minorGridWidth,
|
||||
minorGridEvery: minorGridEvery,
|
||||
majorGridEvery: majorGridEvery,
|
||||
color: colors[colorIndex],
|
||||
alpha: gridAlpha,
|
||||
});
|
||||
|
||||
that.emitUpdate();
|
||||
if (!noUpdate) {
|
||||
that.emitUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
|
@ -236,18 +234,18 @@ GridTool = function(opts) {
|
|||
});
|
||||
|
||||
horizontalGrid.addListener(function(data) {
|
||||
webView.eventBridge.emitScriptEvent(JSON.stringify(data));
|
||||
webView.emitScriptEvent(JSON.stringify(data));
|
||||
selectionDisplay.updateHandles();
|
||||
});
|
||||
|
||||
webView.eventBridge.webEventReceived.connect(function(data) {
|
||||
webView.webEventReceived.connect(function(data) {
|
||||
data = JSON.parse(data);
|
||||
if (data.type == "init") {
|
||||
horizontalGrid.emitUpdate();
|
||||
} else if (data.type == "update") {
|
||||
horizontalGrid.update(data);
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
listeners[i](data);
|
||||
listeners[i] && listeners[i](data);
|
||||
}
|
||||
} else if (data.type == "action") {
|
||||
var action = data.action;
|
||||
|
|
|
@ -110,12 +110,25 @@ Tool = function(properties, selectable, selected) { // selectable and selected a
|
|||
}
|
||||
|
||||
this.select(selected);
|
||||
|
||||
|
||||
this.isButtonDown = false;
|
||||
this.buttonDown = function (down) {
|
||||
if (down !== this.isButtonDown) {
|
||||
properties.subImage.y = (down ? 0 : 1) * properties.subImage.height;
|
||||
Overlays.editOverlay(this.overlay(), { subImage: properties.subImage });
|
||||
this.isButtonDown = down;
|
||||
}
|
||||
}
|
||||
|
||||
this.baseClicked = this.clicked;
|
||||
this.clicked = function(clickedOverlay, update) {
|
||||
if (this.baseClicked(clickedOverlay)) {
|
||||
if (selectable && update) {
|
||||
this.toggle();
|
||||
if (update) {
|
||||
if (selectable) {
|
||||
this.toggle();
|
||||
} else if (properties.showButtonDown) {
|
||||
this.buttonDown(true);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -131,7 +144,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
|
|||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = 0;
|
||||
this.height = ToolBar.TITLE_BAR_HEIGHT;
|
||||
this.height = 0
|
||||
this.backAlpha = 1.0;
|
||||
this.back = Overlays.addOverlay("rectangle", {
|
||||
color: { red: 255, green: 255, blue: 255 },
|
||||
|
@ -327,13 +340,13 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
|
|||
}
|
||||
|
||||
var that = this;
|
||||
this.contains = function (xOrPoint, optionalY) {
|
||||
this.contains = function (xOrPoint, optionalY) { // All four margins are draggable.
|
||||
var x = (optionalY === undefined) ? xOrPoint.x : xOrPoint,
|
||||
y = (optionalY === undefined) ? xOrPoint.y : optionalY;
|
||||
return (that.x <= x) && (x <= (that.x + that.width)) &&
|
||||
(that.y <= y) && (y <= (that.y + that.height));
|
||||
return ((that.x - ToolBar.SPACING) <= x) && (x <= (that.x + that.width + ToolBar.SPACING)) &&
|
||||
((that.y - ToolBar.SPACING) <= y) && (y <= (that.y + that.height));
|
||||
}
|
||||
that.hover = function (enable) { // Can be overriden or extended by clients.
|
||||
that.hover = function (enable) { // Can be overridden or extended by clients.
|
||||
that.isHovering = enable;
|
||||
if (that.back) {
|
||||
Overlays.editOverlay(this.back, {
|
||||
|
@ -376,6 +389,11 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
|
|||
that.mightBeDragging = false;
|
||||
}
|
||||
};
|
||||
this.mouseReleaseEvent = function (event) {
|
||||
for (var tool in that.tools) {
|
||||
that.tools[tool].buttonDown(false);
|
||||
}
|
||||
}
|
||||
this.mouseMove = function (event) {
|
||||
if (!that.mightBeDragging || !event.isLeftButton) {
|
||||
that.mightBeDragging = false;
|
||||
|
@ -399,6 +417,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
|
|||
}
|
||||
};
|
||||
Controller.mousePressEvent.connect(this.mousePressEvent);
|
||||
Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent);
|
||||
Controller.mouseMoveEvent.connect(this.mouseMove);
|
||||
Script.update.connect(that.checkResize);
|
||||
// This compatability hack breaks the model, but makes converting existing scripts easier:
|
||||
|
@ -431,7 +450,6 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
|
|||
}
|
||||
}
|
||||
}
|
||||
ToolBar.SPACING = 4;
|
||||
ToolBar.SPACING = 6;
|
||||
ToolBar.VERTICAL = 0;
|
||||
ToolBar.HORIZONTAL = 1;
|
||||
ToolBar.TITLE_BAR_HEIGHT = 10;
|
||||
|
|
|
@ -28,7 +28,6 @@ colorMix = function(colorA, colorB, mix) {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
scaleLine = function (start, end, scale) {
|
||||
var v = Vec3.subtract(end, start);
|
||||
var length = Vec3.length(v);
|
||||
|
@ -199,7 +198,7 @@ pointInExtents = function(point, minPoint, maxPoint) {
|
|||
* @param Number l The lightness
|
||||
* @return Array The RGB representation
|
||||
*/
|
||||
hslToRgb = function(hsl, hueOffset) {
|
||||
hslToRgb = function(hsl) {
|
||||
var r, g, b;
|
||||
if (hsl.s == 0) {
|
||||
r = g = b = hsl.l; // achromatic
|
||||
|
@ -262,6 +261,16 @@ randInt = function(low, high) {
|
|||
return Math.floor(randFloat(low, high));
|
||||
}
|
||||
|
||||
|
||||
randomColor = function() {
|
||||
return {
|
||||
red: randInt(0, 255),
|
||||
green: randInt(0, 255),
|
||||
blue: randInt(0, 255)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
hexToRgb = function(hex) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
|
|
|
@ -58,7 +58,6 @@ var toolBar = (function() {
|
|||
browseMarketplaceButton;
|
||||
|
||||
function initialize() {
|
||||
ToolBar.SPACING = 16;
|
||||
toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.marketplace.toolbar", function(windowDimensions, toolbar) {
|
||||
return {
|
||||
x: windowDimensions.x - 8 - toolbar.width,
|
||||
|
@ -66,11 +65,18 @@ var toolBar = (function() {
|
|||
};
|
||||
});
|
||||
browseMarketplaceButton = toolBar.addTool({
|
||||
imageURL: toolIconUrl + "marketplace.svg",
|
||||
imageURL: toolIconUrl + "market-01.svg",
|
||||
subImage: {
|
||||
x: 0,
|
||||
y: Tool.IMAGE_WIDTH,
|
||||
width: Tool.IMAGE_WIDTH,
|
||||
height: Tool.IMAGE_HEIGHT
|
||||
},
|
||||
width: toolWidth,
|
||||
height: toolHeight,
|
||||
alpha: 0.9,
|
||||
visible: true,
|
||||
showButtonDown: true
|
||||
});
|
||||
|
||||
toolBar.showTool(browseMarketplaceButton, true);
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
<script type="text/javascript" src="../html/eventBridgeLoader.js"></script>
|
||||
<script type="text/javascript" src="particleExplorer.js?v42"></script>
|
||||
<script>
|
||||
function loaded() {
|
||||
// Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked
|
||||
document.addEventListener("contextmenu", function (event) {
|
||||
event.preventDefault();
|
||||
}, false);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
|
@ -60,7 +66,7 @@ body{
|
|||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body onload="loaded();">
|
||||
<div class="importer">
|
||||
<input type='text' id="importer-input" placeholder="Import: Paste JSON here." onkeypress="handleInputKeyPress(event)">
|
||||
<div class = "exported-props-section">
|
||||
|
|
|
@ -43,10 +43,10 @@ var keysToAllow = [
|
|||
'emitSpeed',
|
||||
'speedSpread',
|
||||
'emitOrientation',
|
||||
'emitDimensios',
|
||||
'emitRadiusStart',
|
||||
'emitDimensions',
|
||||
'polarStart',
|
||||
'polarFinish',
|
||||
'azimuthStart',
|
||||
'azimuthFinish',
|
||||
'emitAcceleration',
|
||||
'accelerationSpread',
|
||||
|
@ -198,6 +198,16 @@ function createColorPicker(key) {
|
|||
settings[key] = colorArray;
|
||||
var controller = gui.addColor(settings, key);
|
||||
controller.onChange(function(value) {
|
||||
// Handle hex colors
|
||||
if(_.isString(value) && value[0] === '#') {
|
||||
const BASE_HEX = 16;
|
||||
var colorRegExResult = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(value);
|
||||
value = [
|
||||
parseInt(colorRegExResult[1], BASE_HEX),
|
||||
parseInt(colorRegExResult[2], BASE_HEX),
|
||||
parseInt(colorRegExResult[3], BASE_HEX)
|
||||
];
|
||||
}
|
||||
var obj = {};
|
||||
obj[key] = convertColorArrayToObject(value);
|
||||
writeVec3ToInterface(obj);
|
||||
|
|
|
@ -26,17 +26,15 @@ ParticleExplorerTool = function() {
|
|||
});
|
||||
|
||||
that.webView.setVisible(true);
|
||||
that.webView.eventBridge.webEventReceived.connect(that.webEventReceived);
|
||||
that.webView.webEventReceived.connect(that.webEventReceived);
|
||||
}
|
||||
|
||||
|
||||
that.destroyWebView = function() {
|
||||
if (!that.webView) {
|
||||
print("EBL CAN'ZT CLOSE WEB VIEW- IT DOESNT EXISTS!")
|
||||
return;
|
||||
}
|
||||
|
||||
print("EBL CLOSING WEB VIEW")
|
||||
that.webView.close();
|
||||
that.webView = null;
|
||||
that.activeParticleEntity = 0;
|
||||
|
|
96
examples/playTestSound.js
Normal file
96
examples/playTestSound.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// playTestSound.js
|
||||
// examples
|
||||
//
|
||||
// Created by Philip Rosedale
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
//
|
||||
// Creates an object in front of you that changes color and plays a light
|
||||
// at the start of a drum clip that loops. As you move away it will tell you in the
|
||||
// log how many meters you are from the source.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var sound = SoundCache.getSound("https://s3.amazonaws.com/hifi-public/sounds/Drums/deepdrum1.wav");
|
||||
|
||||
var position = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 }), Quat.getFront(MyAvatar.orientation));
|
||||
|
||||
var time;
|
||||
var soundPlaying = null;
|
||||
|
||||
var baseColor = { red: 100, green: 100, blue: 100 };
|
||||
var litColor = { red: 255, green: 100, blue: 0 };
|
||||
|
||||
var lightTime = 250;
|
||||
|
||||
var distance = 0.0;
|
||||
|
||||
// Create object for visual reference
|
||||
var box = Entities.addEntity({
|
||||
type: "Box",
|
||||
dimensions: { x: 0.25, y: 0.5, z: 0.25 },
|
||||
color: baseColor,
|
||||
position: position
|
||||
});
|
||||
|
||||
|
||||
function checkSound(deltaTime) {
|
||||
var started = false;
|
||||
if (!sound.downloaded) {
|
||||
return;
|
||||
}
|
||||
if (soundPlaying == null) {
|
||||
soundPlaying = Audio.playSound(sound, {
|
||||
position: position,
|
||||
volume: 1.0,
|
||||
loop: false } );
|
||||
started = true;
|
||||
} else if (!soundPlaying.isPlaying) {
|
||||
soundPlaying.restart();
|
||||
started = true;
|
||||
}
|
||||
if (started) {
|
||||
Entities.editEntity(box, { color: litColor });
|
||||
Entities.addEntity({
|
||||
type: "Light",
|
||||
intensity: 5.0,
|
||||
falloffRadius: 10.0,
|
||||
dimensions: {
|
||||
x: 40,
|
||||
y: 40,
|
||||
z: 40
|
||||
},
|
||||
position: Vec3.sum(position, { x: 0, y: 1, z: 0 }),
|
||||
color: litColor,
|
||||
lifetime: lightTime / 1000
|
||||
});
|
||||
Script.setTimeout(resetColor, lightTime);
|
||||
}
|
||||
var currentDistance = Vec3.distance(MyAvatar.position, position);
|
||||
if (Math.abs(currentDistance - distance) > 1.0) {
|
||||
print("Distance from source: " + currentDistance);
|
||||
distance = currentDistance;
|
||||
}
|
||||
}
|
||||
|
||||
function resetColor() {
|
||||
Entities.editEntity(box, { color: baseColor });
|
||||
}
|
||||
|
||||
|
||||
function scriptEnding() {
|
||||
Entities.deleteEntity(box);
|
||||
if (soundPlaying) {
|
||||
print("stop injector");
|
||||
soundPlaying.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Connect a call back that happens every frame
|
||||
Script.scriptEnding.connect(scriptEnding);
|
||||
Script.update.connect(checkSound);
|
||||
|
229
examples/playa/fireworks/fireworksLaunchButtonEntityScript.js
Normal file
229
examples/playa/fireworks/fireworksLaunchButtonEntityScript.js
Normal file
|
@ -0,0 +1,229 @@
|
|||
//
|
||||
// fireworksLaunchEntityScript.js
|
||||
// examples/playa/fireworks
|
||||
//
|
||||
// Created by Eric Levin on 2/24/16.
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Run this script to spawn a big fireworks launch button that a user can press
|
||||
//
|
||||
// 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 _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.timeToExplosionRange = {
|
||||
min: 2500,
|
||||
max: 4500
|
||||
};
|
||||
};
|
||||
|
||||
Fireworks.prototype = {
|
||||
|
||||
startNearTrigger: function() {
|
||||
_this.shootFireworks();
|
||||
},
|
||||
|
||||
startFarTrigger: function() {
|
||||
_this.shootFireworks();
|
||||
},
|
||||
|
||||
clickReleaseOnEntity: function() {
|
||||
_this.shootFireworks();
|
||||
},
|
||||
|
||||
shootFireworks: function() {
|
||||
// Get launch position
|
||||
var launchPosition = getEntityUserData(_this.entityID).launchPosition || _this.position;
|
||||
|
||||
var numMissles = randInt(1, 3);
|
||||
for(var i = 0; i < numMissles; i++) {
|
||||
_this.shootMissle(launchPosition);
|
||||
}
|
||||
},
|
||||
|
||||
shootMissle: function(launchPosition) {
|
||||
Audio.playSound(_this.launchSound, {
|
||||
position: launchPosition,
|
||||
volume: 0.5
|
||||
});
|
||||
|
||||
var MODEL_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/models/Rocket-2.fbx";
|
||||
var missleDimensions = Vec3.multiply({
|
||||
x: 0.24,
|
||||
y: 0.7,
|
||||
z: 0.24
|
||||
}, randFloat(0.2, 1.5));
|
||||
var missleRotation = Quat.fromPitchYawRollDegrees(randInt(-60, 60), 0, randInt(-60, 60));
|
||||
var missleVelocity = Vec3.multiply(Quat.getUp(missleRotation), randFloat(2, 4));
|
||||
var missleAcceleration = Vec3.multiply(Quat.getUp(missleRotation), randFloat(1, 3));
|
||||
var missle = Entities.addEntity({
|
||||
type: "Model",
|
||||
modelURL: MODEL_URL,
|
||||
position: launchPosition,
|
||||
rotation: missleRotation,
|
||||
dimensions: missleDimensions,
|
||||
damping: 0,
|
||||
dynamic: true,
|
||||
lifetime: 20, // Just in case
|
||||
velocity: missleVelocity,
|
||||
acceleration: missleAcceleration,
|
||||
angularVelocity: {
|
||||
x: 0,
|
||||
y: randInt(-1, 1),
|
||||
z: 0
|
||||
},
|
||||
angularDamping: 0,
|
||||
visible: false
|
||||
});
|
||||
|
||||
var smokeTrailPosition = Vec3.sum(launchPosition, Vec3.multiply(-missleDimensions.y / 2 + 0.1, Quat.getUp(missleRotation)));
|
||||
var smoke = Entities.addEntity({
|
||||
type: "ParticleEffect",
|
||||
position: smokeTrailPosition,
|
||||
lifespan: 10,
|
||||
lifetime: 20,
|
||||
name: "Smoke Trail",
|
||||
maxParticles: 3000,
|
||||
emitRate: 80,
|
||||
emitSpeed: 0,
|
||||
speedSpread: 0,
|
||||
dimensions: {
|
||||
x: 1000,
|
||||
y: 1000,
|
||||
z: 1000
|
||||
},
|
||||
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,
|
||||
parentID: missle,
|
||||
});
|
||||
|
||||
|
||||
Script.setTimeout(function() {
|
||||
Entities.editEntity(smoke, {
|
||||
parentID: null,
|
||||
isEmitting: false
|
||||
});
|
||||
|
||||
var explodeBasePosition = Entities.getEntityProperties(missle, "position").position;
|
||||
|
||||
Entities.deleteEntity(missle);
|
||||
// Explode 1 firework immediately
|
||||
_this.explodeFirework(explodeBasePosition);
|
||||
var numAdditionalFireworks = randInt(1, 5);
|
||||
for (var i = 0; i < numAdditionalFireworks; i++) {
|
||||
Script.setTimeout(function() {
|
||||
var explodePosition = Vec3.sum(explodeBasePosition, {x: randFloat(-3, 3), y: randFloat(-3, 3), z: randFloat(-3, 3)});
|
||||
_this.explodeFirework(explodePosition);
|
||||
}, randInt(0, 1000))
|
||||
}
|
||||
}, randFloat(_this.timeToExplosionRange.min, _this.timeToExplosionRange.max));
|
||||
|
||||
|
||||
},
|
||||
|
||||
explodeFirework: function(explodePosition) {
|
||||
// We just exploded firework, so stop emitting its fire and smoke
|
||||
|
||||
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;
|
||||
print("EBL RELOAD ENTITY SCRIPT!!!");
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// entity scripts always need to return a newly constructed object of our type
|
||||
return new Fireworks();
|
||||
});
|
46
examples/playa/fireworks/fireworksLaunchButtonSpawner.js
Normal file
46
examples/playa/fireworks/fireworksLaunchButtonSpawner.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// fireworksLaunchButtonSpawner.js
|
||||
// examples/playa/fireworks
|
||||
//
|
||||
// Created by Eric Levina on 2/24/16.
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Run this script to spawn a big fireworks launch button that a user can press
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
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)));
|
||||
|
||||
// Math.random ensures no caching of script
|
||||
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: "launch pad",
|
||||
modelURL: MODEL_URL,
|
||||
position: center,
|
||||
dimensions: {
|
||||
x: 0.98,
|
||||
y: 1.16,
|
||||
z: 0.98
|
||||
},
|
||||
script: SCRIPT_URL,
|
||||
userData: JSON.stringify({
|
||||
launchPosition: {x: 1, y: 1.8, z: -20.9},
|
||||
grabbableKey: {
|
||||
wantsTrigger: true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(launchButton);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
|
@ -120,23 +120,95 @@ function menuItemEvent(menuItem) {
|
|||
if (menuItem.endsWith(" for Output")) {
|
||||
var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output");
|
||||
print("output audio selection..." + selectedDevice);
|
||||
Menu.menuItemEvent.disconnect(menuItemEvent);
|
||||
Menu.setIsOptionChecked(selectedOutputMenu, false);
|
||||
selectedOutputMenu = menuItem;
|
||||
Menu.setIsOptionChecked(selectedOutputMenu, true);
|
||||
if (AudioDevice.setOutputDevice(selectedDevice)) {
|
||||
Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice);
|
||||
}
|
||||
Menu.menuItemEvent.connect(menuItemEvent);
|
||||
} else if (menuItem.endsWith(" for Input")) {
|
||||
var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input");
|
||||
print("input audio selection..." + selectedDevice);
|
||||
Menu.menuItemEvent.disconnect(menuItemEvent);
|
||||
Menu.setIsOptionChecked(selectedInputMenu, false);
|
||||
selectedInputMenu = menuItem;
|
||||
Menu.setIsOptionChecked(selectedInputMenu, true);
|
||||
if (AudioDevice.setInputDevice(selectedDevice)) {
|
||||
Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice);
|
||||
}
|
||||
Menu.menuItemEvent.connect(menuItemEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu.menuItemEvent.connect(menuItemEvent);
|
||||
|
||||
// Some HMDs (like Oculus CV1) have a built in audio device. If they
|
||||
// do, then this function will handle switching to that device automatically
|
||||
// when you goActive with the HMD active.
|
||||
var wasHmdMounted = false; // assume it's un-mounted to start
|
||||
var switchedAudioInputToHMD = false;
|
||||
var switchedAudioOutputToHMD = false;
|
||||
var previousSelectedInputAudioDevice = "";
|
||||
var previousSelectedOutputAudioDevice = "";
|
||||
|
||||
function restoreAudio() {
|
||||
if (switchedAudioInputToHMD) {
|
||||
print("switching back from HMD preferred audio input to:" + previousSelectedInputAudioDevice);
|
||||
menuItemEvent("Use " + previousSelectedInputAudioDevice + " for Input");
|
||||
}
|
||||
if (switchedAudioOutputToHMD) {
|
||||
print("switching back from HMD preferred audio output to:" + previousSelectedOutputAudioDevice);
|
||||
menuItemEvent("Use " + previousSelectedOutputAudioDevice + " for Output");
|
||||
}
|
||||
}
|
||||
|
||||
function checkHMDAudio() {
|
||||
// Mounted state is changing... handle switching
|
||||
if (HMD.mounted != wasHmdMounted) {
|
||||
print("HMD mounted changed...");
|
||||
|
||||
// We're putting the HMD on... switch to those devices
|
||||
if (HMD.mounted) {
|
||||
print("NOW mounted...");
|
||||
var hmdPreferredAudioInput = HMD.preferredAudioInput();
|
||||
var hmdPreferredAudioOutput = HMD.preferredAudioOutput();
|
||||
print("hmdPreferredAudioInput:" + hmdPreferredAudioInput);
|
||||
print("hmdPreferredAudioOutput:" + hmdPreferredAudioOutput);
|
||||
|
||||
|
||||
var hmdHasPreferredAudio = (hmdPreferredAudioInput !== "") || (hmdPreferredAudioOutput !== "");
|
||||
if (hmdHasPreferredAudio) {
|
||||
print("HMD has preferred audio!");
|
||||
previousSelectedInputAudioDevice = Settings.getValue(INPUT_DEVICE_SETTING);
|
||||
previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING);
|
||||
print("previousSelectedInputAudioDevice:" + previousSelectedInputAudioDevice);
|
||||
print("previousSelectedOutputAudioDevice:" + previousSelectedOutputAudioDevice);
|
||||
if (hmdPreferredAudioInput != previousSelectedInputAudioDevice && hmdPreferredAudioInput !== "") {
|
||||
print("switching to HMD preferred audio input to:" + hmdPreferredAudioInput);
|
||||
switchedAudioInputToHMD = true;
|
||||
menuItemEvent("Use " + hmdPreferredAudioInput + " for Input");
|
||||
}
|
||||
if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice && hmdPreferredAudioOutput !== "") {
|
||||
print("switching to HMD preferred audio output to:" + hmdPreferredAudioOutput);
|
||||
switchedAudioOutputToHMD = true;
|
||||
menuItemEvent("Use " + hmdPreferredAudioOutput + " for Output");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("HMD NOW un-mounted...");
|
||||
restoreAudio();
|
||||
}
|
||||
}
|
||||
wasHmdMounted = HMD.mounted;
|
||||
}
|
||||
|
||||
Script.update.connect(checkHMDAudio);
|
||||
|
||||
Script.scriptEnding.connect(function () {
|
||||
restoreAudio();
|
||||
Menu.menuItemEvent.disconnect(menuItemEvent);
|
||||
Script.update.disconnect(checkHMDAudio);
|
||||
});
|
||||
|
|
125
examples/shaders/rainyDayNightSkybox.fs
Normal file
125
examples/shaders/rainyDayNightSkybox.fs
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
|
|
31
examples/tests/basicEntityTest/entitySpawner.js
Normal file
31
examples/tests/basicEntityTest/entitySpawner.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
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)));
|
||||
|
||||
// Math.random ensures no caching of script
|
||||
var SCRIPT_URL = Script.resolvePath("myEntityScript.js")
|
||||
|
||||
var myEntity = Entities.addEntity({
|
||||
type: "Sphere",
|
||||
color: {
|
||||
red: 200,
|
||||
green: 10,
|
||||
blue: 200
|
||||
},
|
||||
position: center,
|
||||
dimensions: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1
|
||||
},
|
||||
script: SCRIPT_URL
|
||||
})
|
||||
|
||||
|
||||
function cleanup() {
|
||||
// Entities.deleteEntity(myEntity);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
24
examples/tests/basicEntityTest/myEntityScript.js
Normal file
24
examples/tests/basicEntityTest/myEntityScript.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
(function() {
|
||||
var _this;
|
||||
MyEntity = function() {
|
||||
_this = this;
|
||||
|
||||
};
|
||||
|
||||
MyEntity.prototype = {
|
||||
|
||||
|
||||
preload: function(entityID) {
|
||||
this.entityID = entityID;
|
||||
var randNum = Math.random().toFixed(3);
|
||||
print("EBL PRELOAD ENTITY SCRIPT!!!", randNum)
|
||||
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
|
||||
// entity scripts always need to return a newly constructed object of our type
|
||||
return new MyEntity();
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
|
||||
(function() {
|
||||
Script.include("../../libraries/virtualBaton.js");
|
||||
|
||||
var baton;
|
||||
|
||||
var _this;
|
||||
BatonSoundEntity = function() {
|
||||
_this = this;
|
||||
_this.drumSound = SoundCache.getSound("https://s3.amazonaws.com/hifi-public/sounds/Drums/deepdrum1.wav");
|
||||
_this.injectorOptions = {position: MyAvatar.position, loop: false, volume: 1};
|
||||
_this.soundIntervalConnected = false;
|
||||
_this.batonDebugModel = Entities.addEntity({
|
||||
type: "Box",
|
||||
color: {red: 200, green: 10, blue: 200},
|
||||
position: Vec3.sum(MyAvatar.position, {x: 0, y: 1, z: 0}),
|
||||
dimensions: {x: 0.5, y: 1, z: 0},
|
||||
parentID: MyAvatar.sessionUUID,
|
||||
visible: false
|
||||
});
|
||||
};
|
||||
|
||||
function startUpdate() {
|
||||
// We are claiming the baton! So start our clip
|
||||
if (!_this.soundInjector) {
|
||||
// This client hasn't created their injector yet so create one
|
||||
_this.soundInjector = Audio.playSound(_this.drumSound, _this.injectorOptions);
|
||||
} else {
|
||||
// We already have our injector so just restart it
|
||||
_this.soundInjector.restart();
|
||||
}
|
||||
print("EBL START UPDATE");
|
||||
Entities.editEntity(_this.batonDebugModel, {visible: true});
|
||||
_this.playSoundInterval = Script.setInterval(function() {
|
||||
_this.soundInjector.restart();
|
||||
}, _this.drumSound.duration * 1000); // Duration is in seconds so convert to ms
|
||||
_this.soundIntervalConnected = true;
|
||||
}
|
||||
|
||||
function stopUpdateAndReclaim() {
|
||||
print("EBL STOP UPDATE AND RECLAIM")
|
||||
// when the baton is release
|
||||
if (_this.soundIntervalConnected === true) {
|
||||
Script.clearInterval(_this.playSoundInterval);
|
||||
_this.soundIntervalConnected = false;
|
||||
print("EBL CLEAR INTERVAL")
|
||||
}
|
||||
Entities.editEntity(_this.batonDebugModel, {visible: false});
|
||||
// hook up callbacks to the baton
|
||||
baton.claim(startUpdate, stopUpdateAndReclaim);
|
||||
}
|
||||
|
||||
BatonSoundEntity.prototype = {
|
||||
|
||||
|
||||
preload: function(entityID) {
|
||||
_this.entityID = entityID;
|
||||
print("EBL PRELOAD ENTITY SCRIPT!!!");
|
||||
baton = virtualBaton({
|
||||
// One winner for each entity
|
||||
batonName: "io.highfidelity.soundEntityBatonTest:" + _this.entityID,
|
||||
// debugFlow: true
|
||||
});
|
||||
stopUpdateAndReclaim();
|
||||
},
|
||||
|
||||
unload: function() {
|
||||
print("EBL UNLOAD");
|
||||
// baton.release();
|
||||
baton.unload();
|
||||
Entities.deleteEntity(_this.batonDebugModel);
|
||||
if (_this.soundIntervalConnected === true) {
|
||||
Script.clearInterval(_this.playSoundInterval);
|
||||
_this.soundIntervalConnected = false;
|
||||
_this.soundInjector.stop();
|
||||
delete _this.soundInjector;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
// entity scripts always need to return a newly constructed object of our type
|
||||
return new BatonSoundEntity();
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
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)));
|
||||
|
||||
// Math.random ensures no caching of script
|
||||
var SCRIPT_URL = Script.resolvePath("batonSoundTestEntityScript.js")
|
||||
|
||||
var soundEntity = Entities.addEntity({
|
||||
type: "Box",
|
||||
color: {
|
||||
red: 200,
|
||||
green: 10,
|
||||
blue: 10
|
||||
},
|
||||
position: center,
|
||||
dimensions: {
|
||||
x: 0.1,
|
||||
y: 0.1,
|
||||
z: 0.1
|
||||
},
|
||||
script: SCRIPT_URL
|
||||
});
|
||||
|
||||
|
||||
function cleanup() {
|
||||
// Entities.deleteEntity(soundEntity);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
BIN
examples/tests/cube_texture.png
Normal file
BIN
examples/tests/cube_texture.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.8 MiB |
165
examples/tests/mat4test.js
Normal file
165
examples/tests/mat4test.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
//
|
||||
// mat4test.js
|
||||
// examples/tests
|
||||
//
|
||||
// Created by Anthony Thibault on 2016/3/7
|
||||
// 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 IDENTITY = {r0c0: 1, r0c1: 0, r0c2: 0, r0c3: 0,
|
||||
r1c0: 0, r1c1: 1, r1c2: 0, r1c3: 0,
|
||||
r2c0: 0, r2c1: 0, r2c2: 1, r2c3: 0,
|
||||
r3c0: 0, r3c1: 0, r3c2: 0, r3c3: 1};
|
||||
|
||||
var ROT_ZERO = {x: 0, y: 0, z: 0, w: 1};
|
||||
var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0};
|
||||
|
||||
var ONE = {x: 1, y: 1, z: 1};
|
||||
var ZERO = {x: 0, y: 0, z: 0};
|
||||
var ONE_TWO_THREE = {x: 1, y: 2, z: 3};
|
||||
var ONE_HALF = {x: 0.5, y: 0.5, z: 0.5};
|
||||
|
||||
var EPSILON = 0.000001;
|
||||
|
||||
function mat4FuzzyEqual(a, b) {
|
||||
var r, c;
|
||||
for (r = 0; r < 4; r++) {
|
||||
for (c = 0; c < 4; c++) {
|
||||
if (Math.abs(a["r" + r + "c" + c] - b["r" + r + "c" + c]) > EPSILON) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function vec3FuzzyEqual(a, b) {
|
||||
if (Math.abs(a.x - b.x) > EPSILON ||
|
||||
Math.abs(a.y - b.y) > EPSILON ||
|
||||
Math.abs(a.z - b.z) > EPSILON) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function quatFuzzyEqual(a, b) {
|
||||
if (Math.abs(a.x - b.x) > EPSILON ||
|
||||
Math.abs(a.y - b.y) > EPSILON ||
|
||||
Math.abs(a.z - b.z) > EPSILON ||
|
||||
Math.abs(a.w - b.w) > EPSILON) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var failureCount = 0;
|
||||
var testCount = 0;
|
||||
function assert(test) {
|
||||
testCount++;
|
||||
if (!test) {
|
||||
print("MAT4 TEST " + testCount + " failed!");
|
||||
failureCount++;
|
||||
}
|
||||
}
|
||||
|
||||
function testCreate() {
|
||||
var test0 = Mat4.createFromScaleRotAndTrans(ONE, {x: 0, y: 0, z: 0, w: 1}, ZERO);
|
||||
assert(mat4FuzzyEqual(test0, IDENTITY));
|
||||
|
||||
var test1 = Mat4.createFromRotAndTrans({x: 0, y: 0, z: 0, w: 1}, ZERO);
|
||||
assert(mat4FuzzyEqual(test1, IDENTITY));
|
||||
|
||||
var test2 = Mat4.createFromRotAndTrans(ROT_Y_180, ONE_TWO_THREE);
|
||||
assert(mat4FuzzyEqual(test2, {r0c0: -1, r0c1: 0, r0c2: 0, r0c3: 1,
|
||||
r1c0: 0, r1c1: 1, r1c2: 0, r1c3: 2,
|
||||
r2c0: 0, r2c1: 0, r2c2: -1, r2c3: 3,
|
||||
r3c0: 0, r3c1: 0, r3c2: 0, r3c3: 1}));
|
||||
|
||||
var test3 = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE);
|
||||
assert(mat4FuzzyEqual(test3, {r0c0: -0.5, r0c1: 0, r0c2: 0, r0c3: 1,
|
||||
r1c0: 0, r1c1: 0.5, r1c2: 0, r1c3: 2,
|
||||
r2c0: 0, r2c1: 0, r2c2: -0.5, r2c3: 3,
|
||||
r3c0: 0, r3c1: 0, r3c2: 0, r3c3: 1}));
|
||||
}
|
||||
|
||||
function testExtractTranslation() {
|
||||
var test0 = Mat4.extractTranslation(IDENTITY);
|
||||
assert(vec3FuzzyEqual(ZERO, test0));
|
||||
|
||||
var test1 = Mat4.extractTranslation(Mat4.createFromRotAndTrans(ROT_Y_180, ONE_TWO_THREE));
|
||||
assert(vec3FuzzyEqual(ONE_TWO_THREE, test1));
|
||||
}
|
||||
|
||||
function testExtractRotation() {
|
||||
var test0 = Mat4.extractRotation(IDENTITY);
|
||||
assert(quatFuzzyEqual(ROT_ZERO, test0));
|
||||
|
||||
var test1 = Mat4.extractRotation(Mat4.createFromRotAndTrans(ROT_Y_180, ONE_TWO_THREE));
|
||||
assert(quatFuzzyEqual(ROT_Y_180, test1));
|
||||
}
|
||||
|
||||
function testExtractScale() {
|
||||
var test0 = Mat4.extractScale(IDENTITY);
|
||||
assert(vec3FuzzyEqual(ONE, test0));
|
||||
|
||||
var test1 = Mat4.extractScale(Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE));
|
||||
assert(vec3FuzzyEqual(ONE_HALF, test1));
|
||||
|
||||
var test2 = Mat4.extractScale(Mat4.createFromScaleRotAndTrans(ONE_TWO_THREE, ROT_ZERO, ONE_TWO_THREE));
|
||||
assert(vec3FuzzyEqual(ONE_TWO_THREE, test2));
|
||||
}
|
||||
|
||||
function testTransformPoint() {
|
||||
var test0 = Mat4.transformPoint(IDENTITY, ONE);
|
||||
assert(vec3FuzzyEqual(ONE, test0));
|
||||
|
||||
var m = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE);
|
||||
var test1 = Mat4.transformPoint(m, ONE);
|
||||
assert(vec3FuzzyEqual({x: 0.5, y: 2.5, z: 2.5}, test1));
|
||||
}
|
||||
|
||||
function testTransformVector() {
|
||||
var test0 = Mat4.transformVector(IDENTITY, ONE);
|
||||
assert(vec3FuzzyEqual(ONE, test0));
|
||||
|
||||
var m = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE);
|
||||
var test1 = Mat4.transformVector(m, ONE);
|
||||
assert(vec3FuzzyEqual({x: -0.5, y: 0.5, z: -0.5}, test1));
|
||||
}
|
||||
|
||||
function testInverse() {
|
||||
var test0 = IDENTITY;
|
||||
assert(mat4FuzzyEqual(IDENTITY, Mat4.multiply(test0, Mat4.inverse(test0))));
|
||||
|
||||
var test1 = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE);
|
||||
assert(mat4FuzzyEqual(IDENTITY, Mat4.multiply(test1, Mat4.inverse(test1))));
|
||||
|
||||
var test2 = Mat4.extractScale(Mat4.createFromScaleRotAndTrans(ONE_TWO_THREE, ROT_ZERO, ONE_TWO_THREE));
|
||||
assert(mat4FuzzyEqual(IDENTITY, Mat4.multiply(test2, Mat4.inverse(test2))));
|
||||
}
|
||||
|
||||
function testFront() {
|
||||
var test0 = IDENTITY;
|
||||
assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getFront(test0)));
|
||||
|
||||
var test1 = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE);
|
||||
assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getFront(test1)));
|
||||
}
|
||||
|
||||
function testMat4() {
|
||||
testCreate();
|
||||
testExtractTranslation();
|
||||
testExtractRotation();
|
||||
testExtractScale();
|
||||
testTransformPoint();
|
||||
testTransformVector();
|
||||
testInverse();
|
||||
testFront();
|
||||
|
||||
print("MAT4 TEST complete! (" + (testCount - failureCount) + "/" + testCount + ") tests passed!");
|
||||
}
|
||||
|
||||
testMat4();
|
101
examples/tests/particleOrientationTest.js
Normal file
101
examples/tests/particleOrientationTest.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
//
|
||||
// particleOrientationTest.js
|
||||
// examples/tests
|
||||
//
|
||||
// Created by Piper.Peppercorn.
|
||||
// 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
|
||||
//
|
||||
|
||||
var emitterBone = 'Head'
|
||||
var particleEntities = [];
|
||||
|
||||
function emitter(jointName) {
|
||||
var jointID = MyAvatar.jointNames.indexOf(jointName);
|
||||
var newEmitter = Entities.addEntity({
|
||||
name: 'particleEmitter ' + jointName,
|
||||
type: 'ParticleEffect',
|
||||
emitterShouldTrail: true,
|
||||
textures: 'https://dl.dropboxusercontent.com/u/96759331/ParticleTest.png',
|
||||
position: Vec3.sum(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointID), MyAvatar.position),
|
||||
parentJointIndex: jointID,
|
||||
position: MyAvatar.getJointPosition(jointName),
|
||||
color: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
isEmitting: 1,
|
||||
maxParticles: 1,
|
||||
lifespan: 2.0
|
||||
,
|
||||
emitRate: 1,
|
||||
emitSpeed: 0.0,
|
||||
speedSpread: 0.0,
|
||||
/*
|
||||
emitOrientation: {
|
||||
x: -0.7035577893257141,
|
||||
y: -0.000015259007341228426,
|
||||
z: -0.000015259007341228426,
|
||||
w: 1.7106381058692932
|
||||
},
|
||||
*/
|
||||
emitOrientation: {
|
||||
x:0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
w: 1
|
||||
},
|
||||
emitRadiusStart: 0,
|
||||
polarStart: 0,
|
||||
polarFinish: 0,
|
||||
azimuthFinish: 3.1415927410125732,
|
||||
emitAcceleration: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
accelerationSpread: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
},
|
||||
particleRadius: 2.0,
|
||||
radiusSpread: 1.0,
|
||||
radiusStart: 2.0,
|
||||
radiusFinish: 2.0,
|
||||
colorSpread: {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 0
|
||||
},
|
||||
colorStart: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
colorFinish: {
|
||||
red: 255,
|
||||
green: 255,
|
||||
blue: 255
|
||||
},
|
||||
alpha: 1,
|
||||
alphaSpread: 0,
|
||||
alphaStart: 1,
|
||||
alphaFinish: 1
|
||||
});
|
||||
return newEmitter;
|
||||
}
|
||||
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
for (var i = 0; i < particleEntities.length; i++) {
|
||||
// Fixes a crash on shutdown:
|
||||
Entities.editEntity(particleEntities[i], { parentID: '' });
|
||||
Entities.deleteEntity(particleEntities[i]);
|
||||
}
|
||||
});
|
||||
|
||||
particleEntities.push(emitter(emitterBone));
|
|
@ -2,32 +2,18 @@ print("Launching web window");
|
|||
|
||||
var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html")
|
||||
webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false);
|
||||
print("JS Side window: " + webWindow);
|
||||
print("JS Side bridge: " + webWindow.eventBridge);
|
||||
webWindow.eventBridge.webEventReceived.connect(function(data) {
|
||||
webWindow.webEventReceived.connect(function(data) {
|
||||
print("JS Side event received: " + data);
|
||||
});
|
||||
|
||||
var titles = ["A", "B", "C"];
|
||||
var titleIndex = 0;
|
||||
|
||||
Script.setInterval(function() {
|
||||
webWindow.eventBridge.emitScriptEvent("JS Event sent");
|
||||
var size = webWindow.size;
|
||||
var position = webWindow.position;
|
||||
print("Window url: " + webWindow.url)
|
||||
print("Window visible: " + webWindow.visible)
|
||||
print("Window size: " + size.x + "x" + size.y)
|
||||
print("Window pos: " + position.x + "x" + position.y)
|
||||
webWindow.setVisible(!webWindow.visible);
|
||||
webWindow.setTitle(titles[titleIndex]);
|
||||
webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100);
|
||||
titleIndex += 1;
|
||||
titleIndex %= titles.length;
|
||||
}, 2 * 1000);
|
||||
var message = [ Math.random(), Math.random() ];
|
||||
print("JS Side sending: " + message);
|
||||
webWindow.emitScriptEvent(message);
|
||||
}, 5 * 1000);
|
||||
|
||||
Script.setTimeout(function() {
|
||||
print("Closing script");
|
||||
Script.scriptEnding.connect(function(){
|
||||
webWindow.close();
|
||||
Script.stop();
|
||||
}, 15 * 1000)
|
||||
webWindow.deleteLater();
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// rapidProceduralChangeTest.js
|
||||
// examples/tests/rapidProceduralChange
|
||||
//
|
||||
// Created by Eric Levin on 3/9/2016.
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// This test creates primitives with fragment shaders and rapidly updates its uniforms, as well as a skybox.
|
||||
// For the test to pass:
|
||||
// - The primitives (cube and sphere) should update at rate of update loop, cycling through red values.
|
||||
// - The skymap should do the same, although its periodicity may be different.
|
||||
//
|
||||
// Under the hood, the primitives are driven by a uniform, while the skymap is driven by a timer.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var orientation = Camera.getOrientation();
|
||||
orientation = Quat.safeEulerAngles(orientation);
|
||||
orientation.x = 0;
|
||||
orientation = Quat.fromVec3Degrees(orientation);
|
||||
|
||||
var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
|
||||
centerUp.y += 0.5;
|
||||
var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation)));
|
||||
centerDown.y -= 0.5;
|
||||
|
||||
var ENTITY_SHADER_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/shaders/uniformTest.fs";
|
||||
var SKYBOX_SHADER_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/shaders/timerTest.fs";
|
||||
|
||||
var entityData = {
|
||||
ProceduralEntity: {
|
||||
shaderUrl: ENTITY_SHADER_URL,
|
||||
uniforms: { red: 0.0 }
|
||||
}
|
||||
};
|
||||
var skyboxData = {
|
||||
ProceduralEntity: {
|
||||
shaderUrl: SKYBOX_SHADER_URL,
|
||||
uniforms: { red: 0.0 }
|
||||
}
|
||||
};
|
||||
|
||||
var testBox = Entities.addEntity({
|
||||
type: "Box",
|
||||
dimensions: { x: 0.5, y: 0.5, z: 0.5 },
|
||||
position: centerUp,
|
||||
userData: JSON.stringify(entityData)
|
||||
});
|
||||
var testSphere = Entities.addEntity({
|
||||
type: "Sphere",
|
||||
dimensions: { x: 0.5, y: 0.5, z: 0.5 },
|
||||
position: centerDown,
|
||||
userData: JSON.stringify(entityData)
|
||||
});
|
||||
var testZone = Entities.addEntity({
|
||||
type: "Zone",
|
||||
dimensions: { x: 50, y: 50, z: 50 },
|
||||
position: MyAvatar.position,
|
||||
userData: JSON.stringify(skyboxData),
|
||||
backgroundMode: "skybox",
|
||||
skybox: { url: "http://kyoub.googlecode.com/svn/trunk/KYouB/textures/skybox_test.png" }
|
||||
});
|
||||
|
||||
|
||||
var currentTime = 0;
|
||||
|
||||
function update(deltaTime) {
|
||||
var red = (Math.sin(currentTime) + 1) / 2;
|
||||
entityData.ProceduralEntity.uniforms.red = red;
|
||||
skyboxData.ProceduralEntity.uniforms.red = red;
|
||||
entityEdit = { userData: JSON.stringify(entityData) };
|
||||
skyboxEdit = { userData: JSON.stringify(skyboxData) };
|
||||
|
||||
Entities.editEntity(testBox, entityEdit);
|
||||
Entities.editEntity(testSphere, entityEdit);
|
||||
Entities.editEntity(testZone, skyboxEdit);
|
||||
|
||||
currentTime += deltaTime;
|
||||
}
|
||||
|
||||
Script.update.connect(update);
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(testBox);
|
||||
Entities.deleteEntity(testSphere);
|
||||
Entities.deleteEntity(testZone);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue