Merge branch 'master' of https://github.com/highfidelity/hifi into 19942-MacInstall

This commit is contained in:
NissimHadar 2018-11-20 08:43:40 -08:00
commit cbfe9f5440
30 changed files with 440 additions and 207 deletions

View file

@ -435,7 +435,11 @@ void AudioMixer::start() {
QCoreApplication::processEvents();
}
int numToRetain = nodeList->size() * (1 - _throttlingRatio);
int numToRetain = -1;
assert(_throttlingRatio >= 0.0f && _throttlingRatio <= 1.0f);
if (_throttlingRatio > EPSILON) {
numToRetain = nodeList->size() * (1.0f - _throttlingRatio);
}
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
// mix across slave threads
auto mixTimer = _mixTiming.timer();

View file

@ -416,7 +416,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// NOTE: Here's where we determine if we are over budget and drop remaining avatars,
// or send minimal avatar data in uncommon case of PALIsOpen.
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
bool overBudget = (identityBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes) > maxAvatarBytesPerFrame;
auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes;
bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame;
if (overBudget) {
if (PALIsOpen) {
_stats.overBudgetAvatars++;
@ -497,8 +498,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
_stats.avatarDataPackingElapsedTime +=
(quint64) chrono::duration_cast<chrono::microseconds>(endAvatarDataPacking - startAvatarDataPacking).count();
// use helper to add any changed traits to our packet list
traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList);
if (!overBudget) {
// use helper to add any changed traits to our packet list
traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList);
}
remainingAvatars--;
}

View file

@ -10,10 +10,85 @@ $(document).ready(function(){
function progressBarHTML(extraClass, label) {
var html = "<div class='progress'>";
html += "<div class='" + extraClass + " progress-bar progress-bar-success progress-bar-striped active' role='progressbar' aria-valuemin='0' aria-valuemax='100'>";
html += label + "<span class='sr-only'></span></div></div>";
html += "<span class='ongoing-msg'></span></div></div>";
return html;
}
function showUploadProgress(title) {
swal({
title: title,
text: progressBarHTML('upload-content-progress', 'Upload'),
html: true,
showConfirmButton: false,
allowEscapeKey: false
});
}
function uploadNextChunk(file, offset, id) {
if (offset == undefined) {
offset = 0;
}
if (id == undefined) {
// Identify this upload session
id = Math.round(Math.random() * 2147483647);
}
var fileSize = file.size;
var filename = file.name;
var CHUNK_SIZE = 1048576; // 1 MiB
var isFinal = Boolean(fileSize - offset <= CHUNK_SIZE);
var nextChunkSize = Math.min(fileSize - offset, CHUNK_SIZE);
var chunk = file.slice(offset, offset + nextChunkSize, file.type);
var chunkFormData = new FormData();
var formItemName = 'restore-file-chunk';
if (offset == 0) {
formItemName = isFinal ? 'restore-file-chunk-only' : 'restore-file-chunk-initial';
} else if (isFinal) {
formItemName = 'restore-file-chunk-final';
}
chunkFormData.append(formItemName, chunk, filename);
var ajaxParams = {
url: '/content/upload',
type: 'POST',
timeout: 30000, // 30 s
headers: {"X-Session-Id": id},
cache: false,
processData: false,
contentType: false,
data: chunkFormData
};
var ajaxObject = $.ajax(ajaxParams);
ajaxObject.fail(function (jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
});
updateProgressBars($('.upload-content-progress'), (offset + nextChunkSize) * 100 / fileSize);
if (!isFinal) {
ajaxObject.done(function (data, textStatus, jqXHR)
{ uploadNextChunk(file, offset + CHUNK_SIZE, id); });
} else {
ajaxObject.done(function(data, textStatus, jqXHR) {
isRestoring = true;
// immediately reload backup information since one should be restoring now
reloadBackupInformation();
swal.close();
});
}
}
function setupBackupUpload() {
// construct the HTML needed for the settings backup panel
var html = "<div class='form-group'><div id='" + UPLOAD_CONTENT_ALLOWED_DIV_ID + "'>";
@ -50,34 +125,10 @@ $(document).ready(function(){
"Restore content",
function() {
var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files');
var file = files[0];
var fileFormData = new FormData();
fileFormData.append('restore-file', files[0]);
showSpinnerAlert("Uploading content to restore");
$.ajax({
url: '/content/upload',
type: 'POST',
timeout: 3600000, // Set timeout to 1h
cache: false,
processData: false,
contentType: false,
data: fileFormData
}).done(function(data, textStatus, jqXHR) {
isRestoring = true;
// immediately reload backup information since one should be restoring now
reloadBackupInformation();
swal.close();
}).fail(function(jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
});
showUploadProgress("Uploading " + file.name);
uploadNextChunk(file);
}
);
});
@ -168,6 +219,11 @@ $(document).ready(function(){
checkBackupStatus();
});
function updateProgressBars($progressBar, value) {
$progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%');
$progressBar.find('.ongoing-msg').html(" " + Math.round(value) + "%");
}
function reloadBackupInformation() {
// make a GET request to get backup information to populate the table
$.ajax({
@ -204,11 +260,6 @@ $(document).ready(function(){
+ "<li><a class='" + BACKUP_DELETE_LINK_CLASS + "' href='#' target='_blank'>Delete</a></li></ul></div></td>";
}
function updateProgressBars($progressBar, value) {
$progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%');
$progressBar.find('.sr-only').html(value + "% Complete");
}
// before we add any new rows and update existing ones
// remove our flag for active rows
$('.' + ACTIVE_BACKUP_ROW_CLASS).removeClass(ACTIVE_BACKUP_ROW_CLASS);

View file

@ -348,6 +348,27 @@ void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise
});
}
void DomainContentBackupManager::recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromUploadedFile", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QString, uploadedFilename));
return;
}
qDebug() << "Recovering from uploaded file -" << uploadedFilename;
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
bool success = recoverFromBackupZip(backupName, uploadedZip);
promise->resolve({
{ "success", success }
});
}
std::vector<BackupItemInfo> DomainContentBackupManager::getAllBackups() {
QDir backupDir { _backupDirectory };

View file

@ -86,6 +86,7 @@ public slots:
void createManualBackup(MiniPromise::Promise promise, const QString& name);
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup);
void recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename);
void deleteBackup(MiniPromise::Promise promise, const QString& backupName);
signals:

View file

@ -2258,46 +2258,18 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// check the file extension to see what kind of file this is
// to make sure we handle this filetype for a content restore
auto dispositionValue = QString(firstFormData.first.value("Content-Disposition"));
auto formDataFilenameRegex = QRegExp("filename=\"(.+)\"");
auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue);
QRegExp formDataFieldsRegex(R":(name="(restore-file.*)".*filename="(.+)"):");
auto matchIndex = formDataFieldsRegex.indexIn(dispositionValue);
QString formItemName = "";
QString uploadedFilename = "";
if (matchIndex != -1) {
uploadedFilename = formDataFilenameRegex.cap(1);
}
if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive)
|| uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second));
// respond with a 200 for success
connection->respond(HTTPConnection::StatusCode200);
} else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) {
auto deferred = makePromise("recoverFromUploadedBackup");
deferred->then([connectionPtr, JSON_MIME_TYPE](QString error, QVariantMap result) {
if (!connectionPtr) {
return;
}
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->recoverFromUploadedBackup(deferred, firstFormData.second);
return true;
} else {
// we don't have handling for this filetype, send back a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
formItemName = formDataFieldsRegex.cap(1);
uploadedFilename = formDataFieldsRegex.cap(2);
}
// Received a chunk
processPendingContent(connection, formItemName, uploadedFilename, firstFormData.second);
} else {
// respond with a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
@ -2546,6 +2518,72 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u
}
}
bool DomainServer::processPendingContent(HTTPConnection* connection, QString itemName, QString filename, QByteArray dataChunk) {
static const QString UPLOAD_SESSION_KEY { "X-Session-Id" };
QByteArray sessionIdBytes = connection->requestHeader(UPLOAD_SESSION_KEY);
int sessionId = sessionIdBytes.toInt();
bool newUpload = itemName == "restore-file" || itemName == "restore-file-chunk-initial" || itemName == "restore-file-chunk-only";
if (filename.endsWith(".zip", Qt::CaseInsensitive)) {
static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" };
if (_pendingContentFiles.find(sessionId) == _pendingContentFiles.end()) {
if (!newUpload) {
return false;
}
std::unique_ptr<QTemporaryFile> newTemp(new QTemporaryFile(TEMPORARY_CONTENT_FILEPATH));
_pendingContentFiles[sessionId] = std::move(newTemp);
} else if (newUpload) {
qCDebug(domain_server) << "New upload received using existing session ID";
_pendingContentFiles[sessionId]->resize(0);
}
QTemporaryFile& _pendingFileContent = *_pendingContentFiles[sessionId];
if (!_pendingFileContent.open()) {
_pendingContentFiles.erase(sessionId);
connection->respond(HTTPConnection::StatusCode400);
return false;
}
_pendingFileContent.seek(_pendingFileContent.size());
_pendingFileContent.write(dataChunk);
_pendingFileContent.close();
// Respond immediately - will timeout if we wait for restore.
connection->respond(HTTPConnection::StatusCode200);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {
auto deferred = makePromise("recoverFromUploadedBackup");
deferred->then([this, sessionId](QString error, QVariantMap result) {
_pendingContentFiles.erase(sessionId);
});
_contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName());
}
} else if (filename.endsWith(".json", Qt::CaseInsensitive)
|| filename.endsWith(".json.gz", Qt::CaseInsensitive)) {
if (_pendingUploadedContents.find(sessionId) == _pendingUploadedContents.end() && !newUpload) {
qCDebug(domain_server) << "Json upload with invalid session ID received";
return false;
}
QByteArray& _pendingUploadedContent = _pendingUploadedContents[sessionId];
_pendingUploadedContent += dataChunk;
connection->respond(HTTPConnection::StatusCode200);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent));
_pendingUploadedContents.erase(sessionId);
}
} else {
connection->respond(HTTPConnection::StatusCode400);
return false;
}
return true;
}
HTTPSConnection* DomainServer::connectionFromReplyWithState(QNetworkReply* reply) {
// grab the UUID state property from the reply
QUuid stateUUID = reply->property(STATE_QUERY_KEY.toLocal8Bit()).toUuid();

View file

@ -20,6 +20,7 @@
#include <QtCore/QStringList>
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QHostAddress>
#include <QAbstractNativeEventFilter>
#include <Assignment.h>
@ -209,6 +210,8 @@ private:
HTTPSConnection* connectionFromReplyWithState(QNetworkReply* reply);
bool processPendingContent(HTTPConnection* connection, QString itemName, QString filename, QByteArray dataChunk);
bool forwardMetaverseAPIRequest(HTTPConnection* connection,
const QString& metaversePath,
const QString& requestSubobject,
@ -281,6 +284,9 @@ private:
QHash<QUuid, QPointer<HTTPSConnection>> _pendingOAuthConnections;
std::unordered_map<int, QByteArray> _pendingUploadedContents;
std::unordered_map<int, std::unique_ptr<QTemporaryFile>> _pendingContentFiles;
QThread _assetClientThread;
};

View file

@ -701,9 +701,9 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.53203323516845703,
"x": -0.59333323516845703,
"y": 0.019300000742077827,
"z": -0.07286686894893646
"z": 0.037454843521118164
},
"modelURL": "meshes/keyboard/SM_key.fbx",
"texture": {
@ -752,7 +752,7 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.59333323516845703,
"x": -0.65333323516845703,
"y": 0.019300000742077827,
"z": 0.037454843521118164
},
@ -777,9 +777,9 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.5103323516845703,
"y": 0.019300000742077827,
"z": -0.127054843521118164
"x": -0.5503323516845703,
"y": 0.019300000742077827,
"z": -0.07282185554504395
},
"modelURL": "meshes/keyboard/SM_enter.fbx",
"texture": {
@ -1479,9 +1479,9 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.53203323516845703,
"x": -0.59333323516845703,
"y": 0.019300000742077827,
"z": -0.07286686894893646
"z": 0.037454843521118164
},
"modelURL": "meshes/keyboard/SM_key.fbx",
"texture": {
@ -1530,7 +1530,7 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.59333323516845703,
"x": -0.65333323516845703,
"y": 0.019300000742077827,
"z": 0.037454843521118164
},
@ -1555,9 +1555,9 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.5103323516845703,
"y": 0.019300000742077827,
"z": -0.127054843521118164
"x": -0.5503323516845703,
"y": 0.019300000742077827,
"z": -0.07282185554504395
},
"modelURL": "meshes/keyboard/SM_enter.fbx",
"texture": {
@ -2305,9 +2305,9 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.53203323516845703,
"x": -0.59333323516845703,
"y": 0.019300000742077827,
"z": -0.07286686894893646
"z": 0.037454843521118164
},
"modelURL": "meshes/keyboard/SM_key.fbx",
"texture": {
@ -2356,7 +2356,7 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.59333323516845703,
"x": -0.65333323516845703,
"y": 0.019300000742077827,
"z": 0.037454843521118164
},
@ -2381,9 +2381,9 @@
"y": 0.04787999764084816
},
"position": {
"x": -0.5103323516845703,
"y": 0.019300000742077827,
"z": -0.127054843521118164
"x": -0.5503323516845703,
"y": 0.019300000742077827,
"z": -0.07282185554504395
},
"modelURL": "meshes/keyboard/SM_enter.fbx",
"texture": {

View file

@ -18,7 +18,7 @@
window.isKeyboardRaised = false;
window.isNumericKeyboard = false;
window.isPasswordField = false;
window.lastActiveElement = null;
window.lastActiveInputElement = null;
function getActiveElement() {
return document.activeElement;
@ -70,11 +70,15 @@
var keyboardRaised = shouldRaiseKeyboard();
var numericKeyboard = shouldSetNumeric();
var passwordField = shouldSetPasswordField();
var activeElement = getActiveElement();
var activeInputElement = null;
// Only set the active input element when there is an input element focussed, otherwise it will scroll on body focus as well.
if (keyboardRaised) {
activeInputElement = getActiveElement();
}
if (isWindowFocused &&
(keyboardRaised !== window.isKeyboardRaised || numericKeyboard !== window.isNumericKeyboard
|| passwordField !== window.isPasswordField || activeElement !== window.lastActiveElement)) {
|| passwordField !== window.isPasswordField || activeInputElement !== window.lastActiveInputElement)) {
if (typeof EventBridge !== "undefined" && EventBridge !== null) {
EventBridge.emitWebEvent(
@ -96,7 +100,7 @@
window.isKeyboardRaised = keyboardRaised;
window.isNumericKeyboard = numericKeyboard;
window.isPasswordField = passwordField;
window.lastActiveElement = activeElement;
window.lastActiveInputElement = activeInputElement;
}
}, POLL_FREQUENCY);

View file

@ -141,6 +141,7 @@ TabletModalWindow {
Component.onDestruction: {
loginKeyboard.raised = false;
KeyboardScriptingInterface.raised = false;
}
Component.onCompleted: {

Binary file not shown.

View file

@ -45,6 +45,7 @@
#include "scripting/HMDScriptingInterface.h"
#include "scripting/WindowScriptingInterface.h"
#include "scripting/SelectionScriptingInterface.h"
#include "scripting/HMDScriptingInterface.h"
#include "DependencyManager.h"
#include "raypick/StylusPointer.h"
@ -54,9 +55,9 @@
static const int LEFT_HAND_CONTROLLER_INDEX = 0;
static const int RIGHT_HAND_CONTROLLER_INDEX = 1;
static const float MALLET_LENGTH = 0.2f;
static const float MALLET_TOUCH_Y_OFFSET = 0.052f;
static const float MALLET_Y_OFFSET = 0.180f;
static const float MALLET_LENGTH = 0.18f;
static const float MALLET_TOUCH_Y_OFFSET = 0.050f;
static const float MALLET_Y_OFFSET = 0.160f;
static const glm::quat MALLET_ROTATION_OFFSET{0.70710678f, 0.0f, -0.70710678f, 0.0f};
static const glm::vec3 MALLET_MODEL_DIMENSIONS{0.03f, MALLET_LENGTH, 0.03f};
@ -65,14 +66,14 @@ static const glm::vec3 MALLET_TIP_OFFSET{0.0f, MALLET_LENGTH - MALLET_TOUCH_Y_OF
static const glm::vec3 Z_AXIS {0.0f, 0.0f, 1.0f};
static const glm::vec3 KEYBOARD_TABLET_OFFSET{0.28f, -0.3f, -0.05f};
static const glm::vec3 KEYBOARD_TABLET_OFFSET{0.30f, -0.38f, -0.04f};
static const glm::vec3 KEYBOARD_TABLET_DEGREES_OFFSET{-45.0f, 0.0f, 0.0f};
static const glm::vec3 KEYBOARD_TABLET_LANDSCAPE_OFFSET{-0.2f, -0.27f, -0.05f};
static const glm::vec3 KEYBOARD_TABLET_LANDSCAPE_DEGREES_OFFSET{-45.0f, 0.0f, -90.0f};
static const glm::vec3 KEYBOARD_AVATAR_OFFSET{-0.6f, 0.3f, -0.7f};
static const glm::vec3 KEYBOARD_AVATAR_DEGREES_OFFSET{0.0f, 180.0f, 0.0f};
static const QString SOUND_FILE = PathUtils::resourcesUrl() + "sounds/keyboard_key.mp3";
static const QString SOUND_FILE = PathUtils::resourcesUrl() + "sounds/keyboardPress.mp3";
static const QString MALLET_MODEL_URL = PathUtils::resourcesUrl() + "meshes/drumstick.fbx";
static const float PULSE_STRENGTH = 0.6f;
@ -221,6 +222,7 @@ Keyboard::Keyboard() {
auto pointerManager = DependencyManager::get<PointerManager>();
auto windowScriptingInterface = DependencyManager::get<WindowScriptingInterface>();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto hmdScriptingInterface = DependencyManager::get<HMDScriptingInterface>();
connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, this, &Keyboard::handleTriggerBegin, Qt::QueuedConnection);
connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, this, &Keyboard::handleTriggerContinue, Qt::QueuedConnection);
connect(pointerManager.data(), &PointerManager::triggerEndOverlay, this, &Keyboard::handleTriggerEnd, Qt::QueuedConnection);
@ -228,6 +230,7 @@ Keyboard::Keyboard() {
connect(pointerManager.data(), &PointerManager::hoverEndOverlay, this, &Keyboard::handleHoverEnd, Qt::QueuedConnection);
connect(myAvatar.get(), &MyAvatar::sensorToWorldScaleChanged, this, &Keyboard::scaleKeyboard, Qt::QueuedConnection);
connect(windowScriptingInterface.data(), &WindowScriptingInterface::domainChanged, [&]() { setRaised(false); });
connect(hmdScriptingInterface.data(), &HMDScriptingInterface::displayModeChanged, [&]() { setRaised(false); });
}
void Keyboard::registerKeyboardHighlighting() {
@ -483,7 +486,7 @@ void Keyboard::handleTriggerBegin(const OverlayID& overlayID, const PointerEvent
AudioInjectorOptions audioOptions;
audioOptions.localOnly = true;
audioOptions.position = keyWorldPosition;
audioOptions.volume = 0.1f;
audioOptions.volume = 0.05f;
AudioInjector::playSoundAndDelete(_keySound, audioOptions);
@ -835,8 +838,8 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) {
_textDisplay = textDisplay;
_ignoreItemsLock.withWriteLock([&] {
_itemsToIgnore.push_back(_textDisplay.overlayID);
_itemsToIgnore.push_back(_anchor.overlayID);
_itemsToIgnore.append(_textDisplay.overlayID);
_itemsToIgnore.append(_anchor.overlayID);
});
_layerIndex = 0;
auto pointerManager = DependencyManager::get<PointerManager>();

View file

@ -535,7 +535,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionVector(const PickRay
bool bestIsFront = false;
bool bestIsTablet = false;
auto tabletIDs = qApp->getTabletIDs();
const QVector<OverlayID> keyboardKeysToDiscard = DependencyManager::get<Keyboard>()->getKeysID();
QMutexLocker locker(&_mutex);
RayToOverlayIntersectionResult result;
QMapIterator<OverlayID, Overlay::Pointer> i(_overlaysWorld);
@ -545,7 +545,8 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionVector(const PickRay
auto thisOverlay = std::dynamic_pointer_cast<Base3DOverlay>(i.value());
if ((overlaysToDiscard.size() > 0 && overlaysToDiscard.contains(thisID)) ||
(overlaysToInclude.size() > 0 && !overlaysToInclude.contains(thisID))) {
(overlaysToInclude.size() > 0 && !overlaysToInclude.contains(thisID)) ||
(keyboardKeysToDiscard.size() > 0 && keyboardKeysToDiscard.contains(thisID))) {
continue;
}

View file

@ -23,11 +23,39 @@ AnimSkeleton::AnimSkeleton(const HFMModel& hfmModel) {
for (auto& joint : hfmModel.joints) {
joints.push_back(joint);
}
buildSkeletonFromJoints(joints);
buildSkeletonFromJoints(joints, hfmModel.jointRotationOffsets);
// we make a copy of the inverseBindMatrices in order to prevent mutating the model bind pose
// when we are dealing with a joint offset in the model
for (int i = 0; i < (int)hfmModel.meshes.size(); i++) {
const HFMMesh& mesh = hfmModel.meshes.at(i);
std::vector<HFMCluster> dummyClustersList;
for (int j = 0; j < mesh.clusters.size(); j++) {
std::vector<glm::mat4> bindMatrices;
// cast into a non-const reference, so we can mutate the FBXCluster
HFMCluster& cluster = const_cast<HFMCluster&>(mesh.clusters.at(j));
HFMCluster localCluster;
localCluster.jointIndex = cluster.jointIndex;
localCluster.inverseBindMatrix = cluster.inverseBindMatrix;
localCluster.inverseBindTransform.evalFromRawMatrix(localCluster.inverseBindMatrix);
// if we have a joint offset in the fst file then multiply its inverse by the
// model cluster inverse bind matrix
if (hfmModel.jointRotationOffsets.contains(cluster.jointIndex)) {
AnimPose localOffset(hfmModel.jointRotationOffsets[cluster.jointIndex], glm::vec3());
localCluster.inverseBindMatrix = (glm::mat4)localOffset.inverse() * cluster.inverseBindMatrix;
localCluster.inverseBindTransform.evalFromRawMatrix(localCluster.inverseBindMatrix);
}
dummyClustersList.push_back(localCluster);
}
_clusterBindMatrixOriginalValues.push_back(dummyClustersList);
}
}
AnimSkeleton::AnimSkeleton(const std::vector<HFMJoint>& joints) {
buildSkeletonFromJoints(joints);
AnimSkeleton::AnimSkeleton(const std::vector<HFMJoint>& joints, const QMap<int, glm::quat> jointOffsets) {
buildSkeletonFromJoints(joints, jointOffsets);
}
int AnimSkeleton::nameToJointIndex(const QString& jointName) const {
@ -166,7 +194,8 @@ void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const {
}
}
void AnimSkeleton::buildSkeletonFromJoints(const std::vector<HFMJoint>& joints) {
void AnimSkeleton::buildSkeletonFromJoints(const std::vector<HFMJoint>& joints, const QMap<int, glm::quat> jointOffsets) {
_joints = joints;
_jointsSize = (int)joints.size();
// build a cache of bind poses
@ -189,7 +218,7 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector<HFMJoint>& joints)
// build relative and absolute default poses
glm::mat4 relDefaultMat = glm::translate(_joints[i].translation) * preRotationTransform * glm::mat4_cast(_joints[i].rotation) * postRotationTransform;
AnimPose relDefaultPose(relDefaultMat);
_relativeDefaultPoses.push_back(relDefaultPose);
int parentIndex = getParentIndex(i);
if (parentIndex >= 0) {
_absoluteDefaultPoses.push_back(_absoluteDefaultPoses[parentIndex] * relDefaultPose);
@ -198,6 +227,16 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector<HFMJoint>& joints)
}
}
for (int k = 0; k < _jointsSize; k++) {
if (jointOffsets.contains(k)) {
AnimPose localOffset(jointOffsets[k], glm::vec3());
_absoluteDefaultPoses[k] = _absoluteDefaultPoses[k] * localOffset;
}
}
// re-compute relative poses
_relativeDefaultPoses = _absoluteDefaultPoses;
convertAbsolutePosesToRelative(_relativeDefaultPoses);
for (int i = 0; i < _jointsSize; i++) {
_jointIndicesByName[_joints[i].name] = i;
}

View file

@ -24,7 +24,8 @@ public:
using ConstPointer = std::shared_ptr<const AnimSkeleton>;
explicit AnimSkeleton(const HFMModel& hfmModel);
explicit AnimSkeleton(const std::vector<HFMJoint>& joints);
explicit AnimSkeleton(const std::vector<HFMJoint>& joints, const QMap<int, glm::quat> jointOffsets);
int nameToJointIndex(const QString& jointName) const;
const QString& getJointName(int jointIndex) const;
int getNumJoints() const;
@ -62,9 +63,10 @@ public:
void dump(const AnimPoseVec& poses) const;
std::vector<int> lookUpJointIndices(const std::vector<QString>& jointNames) const;
const HFMCluster getClusterBindMatricesOriginalValues(const int meshIndex, const int clusterIndex) const { return _clusterBindMatrixOriginalValues[meshIndex][clusterIndex]; }
protected:
void buildSkeletonFromJoints(const std::vector<HFMJoint>& joints);
void buildSkeletonFromJoints(const std::vector<HFMJoint>& joints, const QMap<int, glm::quat> jointOffsets);
std::vector<HFMJoint> _joints;
int _jointsSize { 0 };
@ -76,6 +78,7 @@ protected:
std::vector<int> _nonMirroredIndices;
std::vector<int> _mirrorMap;
QHash<QString, int> _jointIndicesByName;
std::vector<std::vector<HFMCluster>> _clusterBindMatrixOriginalValues;
// no copies
AnimSkeleton(const AnimSkeleton&) = delete;

View file

@ -360,8 +360,10 @@ void Rig::initJointStates(const HFMModel& hfmModel, const glm::mat4& modelOffset
void Rig::reset(const HFMModel& hfmModel) {
_geometryOffset = AnimPose(hfmModel.offset);
_invGeometryOffset = _geometryOffset.inverse();
_animSkeleton = std::make_shared<AnimSkeleton>(hfmModel);
_internalPoseSet._relativePoses.clear();
_internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses();

View file

@ -417,6 +417,30 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
return filepath.mid(filepath.lastIndexOf('/') + 1);
}
QMap<QString, glm::quat> getJointRotationOffsets(const QVariantHash& mapping) {
QMap<QString, glm::quat> jointRotationOffsets;
static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset";
if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) {
auto offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash();
for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
QString jointName = itr.key();
QString line = itr.value().toString();
auto quatCoords = line.split(',');
if (quatCoords.size() == 4) {
float quatX = quatCoords[0].mid(1).toFloat();
float quatY = quatCoords[1].toFloat();
float quatZ = quatCoords[2].toFloat();
float quatW = quatCoords[3].mid(0, quatCoords[3].size() - 1).toFloat();
if (!isNaN(quatX) && !isNaN(quatY) && !isNaN(quatZ) && !isNaN(quatW)) {
glm::quat rotationOffset = glm::quat(quatW, quatX, quatY, quatZ);
jointRotationOffsets.insert(jointName, rotationOffset);
}
}
}
}
return jointRotationOffsets;
}
HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString& url) {
const FBXNode& node = _rootNode;
QMap<QString, ExtractedMesh> meshes;
@ -1793,6 +1817,19 @@ HFMModel* FBXReader::extractHFMModel(const QVariantHash& mapping, const QString&
}
}
}
auto offsets = getJointRotationOffsets(mapping);
hfmModel.jointRotationOffsets.clear();
for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
QString jointName = itr.key();
glm::quat rotationOffset = itr.value();
int jointIndex = hfmModel.getJointIndex(jointName);
if (jointIndex != -1) {
hfmModel.jointRotationOffsets.insert(jointIndex, rotationOffset);
}
qCDebug(modelformat) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset;
}
return hfmModelPtr;
}

View file

@ -311,6 +311,8 @@ public:
QString getModelNameOfMesh(int meshIndex) const;
QList<QString> blendshapeChannelNames;
QMap<int, glm::quat> jointRotationOffsets;
};
};

View file

@ -243,6 +243,13 @@ void GeometryReader::run() {
QMetaObject::invokeMethod(resource.data(), "finishedLoading",
Q_ARG(bool, false));
}
} catch (QString& e) {
qCWarning(modelnetworking) << "Exception while loading model --" << e;
auto resource = _resource.toStrongRef();
if (resource) {
QMetaObject::invokeMethod(resource.data(), "finishedLoading",
Q_ARG(bool, false));
}
}
}

View file

@ -46,8 +46,13 @@ bool OctreeEntitiesFileParser::parseEntities(QVariantMap& parsedEntities) {
bool gotId = false;
bool gotVersion = false;
while (!(gotDataVersion && gotEntities && gotId && gotVersion)) {
if (nextToken() != '"') {
int token = nextToken();
while (true) {
if (token == '}') {
break;
}
else if (token != '"') {
_errorString = "Incorrect key string";
return false;
}
@ -144,15 +149,13 @@ bool OctreeEntitiesFileParser::parseEntities(QVariantMap& parsedEntities) {
return false;
}
if (gotDataVersion && gotEntities && gotId && gotVersion) {
break;
} else if (nextToken() != ',') {
_errorString = "Id/value incorrectly terminated";
return false;
token = nextToken();
if (token == ',') {
token = nextToken();
}
}
if (nextToken() != '}' || nextToken() != -1) {
if (nextToken() != -1) {
_errorString = "Ill-formed end of object";
return false;
}

View file

@ -114,18 +114,22 @@ void CauterizedModel::updateClusterMatrices() {
for (int i = 0; i < (int)_meshStates.size(); i++) {
Model::MeshState& state = _meshStates[i];
const HFMMesh& mesh = hfmModel.meshes.at(i);
int meshIndex = i;
for (int j = 0; j < mesh.clusters.size(); j++) {
const HFMCluster& cluster = mesh.clusters.at(j);
int clusterIndex = j;
if (_useDualQuaternionSkinning) {
auto jointPose = _rig.getJointPose(cluster.jointIndex);
Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans());
Transform clusterTransform;
Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform);
Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform);
state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform);
state.clusterDualQuaternions[j].setCauterizationParameters(0.0f, jointPose.trans());
} else {
auto jointMatrix = _rig.getJointTransform(cluster.jointIndex);
glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]);
glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]);
}
}
}
@ -146,9 +150,11 @@ void CauterizedModel::updateClusterMatrices() {
for (int i = 0; i < _cauterizeMeshStates.size(); i++) {
Model::MeshState& state = _cauterizeMeshStates[i];
const HFMMesh& mesh = hfmModel.meshes.at(i);
int meshIndex = i;
for (int j = 0; j < mesh.clusters.size(); j++) {
const HFMCluster& cluster = mesh.clusters.at(j);
int clusterIndex = j;
if (_useDualQuaternionSkinning) {
if (_cauterizeBoneSet.find(cluster.jointIndex) == _cauterizeBoneSet.end()) {
@ -157,7 +163,7 @@ void CauterizedModel::updateClusterMatrices() {
} else {
Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans());
Transform clusterTransform;
Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform);
Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform);
state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform);
state.clusterDualQuaternions[j].setCauterizationParameters(1.0f, cauterizePose.trans());
}
@ -166,7 +172,7 @@ void CauterizedModel::updateClusterMatrices() {
// not cauterized so just copy the value from the non-cauterized version.
state.clusterMatrices[j] = _meshStates[i].clusterMatrices[j];
} else {
glm_mat4u_mul(cauterizeMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]);
glm_mat4u_mul(cauterizeMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]);
}
}
}

View file

@ -1418,18 +1418,21 @@ void Model::updateClusterMatrices() {
const HFMModel& hfmModel = getHFMModel();
for (int i = 0; i < (int) _meshStates.size(); i++) {
MeshState& state = _meshStates[i];
int meshIndex = i;
const HFMMesh& mesh = hfmModel.meshes.at(i);
for (int j = 0; j < mesh.clusters.size(); j++) {
const HFMCluster& cluster = mesh.clusters.at(j);
int clusterIndex = j;
if (_useDualQuaternionSkinning) {
auto jointPose = _rig.getJointPose(cluster.jointIndex);
Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans());
Transform clusterTransform;
Transform::mult(clusterTransform, jointTransform, cluster.inverseBindTransform);
Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform);
state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform);
} else {
auto jointMatrix = _rig.getJointTransform(cluster.jointIndex);
glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]);
glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]);
}
}
}

View file

@ -46,32 +46,25 @@ void SoftAttachmentModel::updateClusterMatrices() {
for (int i = 0; i < (int) _meshStates.size(); i++) {
MeshState& state = _meshStates[i];
const HFMMesh& mesh = hfmModel.meshes.at(i);
int meshIndex = i;
for (int j = 0; j < mesh.clusters.size(); j++) {
const HFMCluster& cluster = mesh.clusters.at(j);
int clusterIndex = j;
// TODO: cache these look-ups as an optimization
int jointIndexOverride = getJointIndexOverride(cluster.jointIndex);
glm::mat4 jointMatrix;
if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) {
jointMatrix = _rigOverride.getJointTransform(jointIndexOverride);
} else {
jointMatrix = _rig.getJointTransform(cluster.jointIndex);
}
if (_useDualQuaternionSkinning) {
glm::mat4 jointMatrix;
if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) {
jointMatrix = _rigOverride.getJointTransform(jointIndexOverride);
} else {
jointMatrix = _rig.getJointTransform(cluster.jointIndex);
}
glm::mat4 m;
glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, m);
glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, m);
state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(m);
} else {
glm::mat4 jointMatrix;
if (jointIndexOverride >= 0 && jointIndexOverride < _rigOverride.getJointStateCount()) {
jointMatrix = _rigOverride.getJointTransform(jointIndexOverride);
} else {
jointMatrix = _rig.getJointTransform(cluster.jointIndex);
}
glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]);
glm_mat4u_mul(jointMatrix, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindMatrix, state.clusterMatrices[j]);
}
}
}

View file

@ -590,9 +590,6 @@ div.section[collapsed="true"], div.section[collapsed="true"] > .section-header {
background-color: #373737;
}
.section-header {
cursor: pointer;
}
.section-header span {
font-size: 30px;
@ -948,12 +945,12 @@ div.refresh input[type="button"] {
}
.draggable-number.left-arrow {
top: -5px;
right: 106px;
left: 0px;
transform: rotate(180deg);
}
.draggable-number.right-arrow {
top: -5px;
left: 106px;
right: 0px;
}
.draggable-number input[type=number] {
position: absolute;
@ -1123,7 +1120,7 @@ textarea:enabled[scrolling="true"]::-webkit-resizer {
}
body#entity-list-body {
div#grid-section, body#entity-list-body {
padding-bottom: 0;
margin: 16px;
}
@ -1552,14 +1549,15 @@ input.rename-entity {
.container {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
margin-bottom: 8px;
min-height: 28px;
}
.container > label {
margin-top: 6px;
width: 200px;
width: 160px;
min-width: 160px;
max-width: 160px;
}
.container > div.checkbox {
@ -1600,6 +1598,8 @@ input.rename-entity {
.xyz.fstuple, .pyr.fstuple {
position: relative;
left: -12px;
min-width: 50px;
width: 100px;
}
.rgb.fstuple .tuple {
@ -1650,3 +1650,15 @@ input.number-slider {
display: flex;
width: 100%;
}
.spacemode-hidden {
display: none;
}
#placeholder-property-type {
min-width: 0px;
}
.collapse-icon {
cursor: pointer;
}

View file

@ -19,47 +19,52 @@
<script type="text/javascript" src="js/gridControls.js"></script>
</head>
<body onload='loaded();'>
<div id="grid-section">
<div class="section-header">
<label>Editing Grid</label>
</div>
<div class="property checkbox">
<input type='checkbox' id="horiz-grid-visible">
<div id="grid-section" class="section">
<div class="property container">
<label for="horiz-grid-visible">Visible</label>
<div style="width: 100%">
<input type='checkbox' id="horiz-grid-visible" style="width: 100%">
<label for="horiz-grid-visible">&nbsp;</label>
</div>
</div>
<div class="property checkbox">
<input type="checkbox" id="snap-to-grid">
<div class="property container">
<label for="snap-to-grid">Snap entities to grid</label>
<div style="width: 100%">
<input type="checkbox" id="snap-to-grid">
<label for="snap-to-grid">&nbsp;</label>
</div>
</div>
<div class="property">
<div class="property container">
<label for="major-spacing">Major grid size <span class="unit">m</span></label>
<div class="number">
<label for="major-spacing">Major grid size <span class="unit">m</span></label>
<input type="number" id="major-spacing" min="1" step="1" />
</div>
</div>
<div class="property container">
<label for="minor-spacing">Minor grid size <span class="unit">m</span></label>
<div class="number">
<label for="minor-spacing">Minor grid size <span class="unit">m</span></label>
<input type="number" id="minor-spacing" min="0.2" step="0.2" />
</div>
</div>
<div class="property number">
<div class="property container">
<label for="horiz-y">Position (Y axis) <span class="unit">m</span></label>
<input type="number" id="horiz-y" step="0.1" />
</div>
<div class="property rgb">
<div id="grid-color" class="color-picker"></div>
<label>Grid line color</label>
<div class="tuple">
<div><input type="number" class="red" id="grid-color-red"><label for="grid-color-red">Red:</label></div>
<div><input type="number" class="green" id="grid-color-green"><label for="grid-color-green">Green:</label></div>
<div><input type="number" class="blue" id="grid-color-blue"><label for="grid-color-blue">Blue:</label></div>
<div style="width: 100%">
<input type="number" id="horiz-y" step="0.1" />
</div>
</div>
<div class="property">
<span>
<div class="property container">
<label>Grid line color</label>
<div style="width: 100%">
<div id="grid-color" class="color-picker"></div>
</div>
</div>
<div class="property container">
<label>Move Grid</label>
<div style="width: 100%">
<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>
</div>
</body>
</html>
</html>

View file

@ -32,7 +32,7 @@ DraggableNumber.prototype = {
mouseUp: function(event) {
if (event.target === this.elText && this.initialMouseEvent) {
let dx = event.clientX - this.initialMouseEvent.clientX;
if (dx <= DELTA_X_FOCUS_THRESHOLD) {
if (Math.abs(dx) <= DELTA_X_FOCUS_THRESHOLD) {
this.elInput.style.visibility = "visible";
this.elText.style.visibility = "hidden";
}

View file

@ -29,7 +29,7 @@ const ICON_FOR_TYPE = {
const DEGREES_TO_RADIANS = Math.PI / 180.0;
const NO_SELECTION = "w";
const NO_SELECTION = ",";
const PROPERTY_SPACE_MODE = {
ALL: 0,
@ -1158,18 +1158,21 @@ const GROUPS = [
label: "Link",
type: "string",
propertyID: "href",
placeholder: "URL",
},
{
label: "Script",
type: "string",
buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ],
propertyID: "script",
placeholder: "URL",
},
{
label: "Server Script",
type: "string",
buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ],
propertyID: "serverScripts",
placeholder: "URL",
},
{
label: "Server Script Status",
@ -1244,8 +1247,9 @@ const GROUPS = [
showPropertyRule: { "collisionless": "false" },
},
{
label: "Collision sound URL",
label: "Collision Sound",
type: "string",
placeholder: "URL",
propertyID: "collisionSoundURL",
showPropertyRule: { "collisionless": "false" },
},
@ -1500,7 +1504,7 @@ function disableProperties() {
function showPropertyElement(propertyID, show) {
let elProperty = properties[propertyID].elContainer;
elProperty.style.display = show ? "flex" : "none";
elProperty.style.display = show ? "" : "none";
}
function resetProperties() {
@ -1622,10 +1626,11 @@ function updateVisibleSpaceModeProperties() {
if (properties.hasOwnProperty(propertyID)) {
let property = properties[propertyID];
let propertySpaceMode = property.spaceMode;
if (propertySpaceMode !== PROPERTY_SPACE_MODE.ALL) {
showPropertyElement(propertyID, propertySpaceMode === currentSpaceMode);
let elProperty = properties[propertyID].elContainer;
if (propertySpaceMode !== PROPERTY_SPACE_MODE.ALL && propertySpaceMode !== currentSpaceMode) {
elProperty.classList.add('spacemode-hidden');
} else {
showPropertyElement(propertyID, true);
elProperty.classList.remove('spacemode-hidden');
}
}
}
@ -2769,7 +2774,7 @@ function loaded() {
elLegend.appendChild(createElementFromHTML(`<div class="label">${group.label}</div>`));
let elSpan = document.createElement('span');
elSpan.className = ".collapse-icon";
elSpan.className = "collapse-icon";
elSpan.innerText = "M";
elLegend.appendChild(elSpan);
elGroup.appendChild(elLegend);
@ -3309,14 +3314,14 @@ function loaded() {
getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction('textures', false));
// Collapsible sections
let elCollapsible = document.getElementsByClassName("section-header");
let elCollapsible = document.getElementsByClassName("collapse-icon");
let toggleCollapsedEvent = function(event) {
let element = this.parentNode;
let element = this.parentNode.parentNode;
let isCollapsed = element.dataset.collapsed !== "true";
element.dataset.collapsed = isCollapsed ? "true" : false;
element.setAttribute("collapsed", isCollapsed ? "true" : "false");
element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M";
this.textContent = isCollapsed ? "L" : "M";
};
for (let collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) {

View file

@ -83,44 +83,26 @@ function loaded() {
var gridColor = { red: 255, green: 255, blue: 255 };
var elColor = document.getElementById("grid-color");
var elColorRed = document.getElementById("grid-color-red");
var elColorGreen = document.getElementById("grid-color-green");
var elColorBlue = document.getElementById("grid-color-blue");
elColor.style.backgroundColor = "rgb(" + gridColor.red + "," + gridColor.green + "," + gridColor.blue + ")";
elColorRed.value = gridColor.red;
elColorGreen.value = gridColor.green;
elColorBlue.value = gridColor.blue;
var colorChangeFunction = function () {
gridColor = { red: elColorRed.value, green: elColorGreen.value, blue: elColorBlue.value };
elColor.style.backgroundColor = "rgb(" + gridColor.red + "," + gridColor.green + "," + gridColor.blue + ")";
emitUpdate();
};
var colorPickFunction = function (red, green, blue) {
elColorRed.value = red;
elColorGreen.value = green;
elColorBlue.value = blue;
gridColor = { red: red, green: green, blue: blue };
emitUpdate();
};
elColorRed.addEventListener('change', colorChangeFunction);
elColorGreen.addEventListener('change', colorChangeFunction);
elColorBlue.addEventListener('change', colorChangeFunction);
$('#grid-color').colpick({
colorScheme: 'dark',
layout: 'hex',
layout: 'rgbhex',
color: { r: gridColor.red, g: gridColor.green, b: gridColor.blue },
submit: false,
onShow: function (colpick) {
$('#grid-color').attr('active', 'true');
},
onHide: function (colpick) {
$('#grid-color').attr('active', 'false');
},
onSubmit: function (hsb, hex, rgb, el) {
onChange: function (hsb, hex, rgb, el) {
$(el).css('background-color', '#' + hex);
$(el).colpickHide();
colorPickFunction(rgb.r, rgb.g, rgb.b);
}
});