From 28b3eef7aa57f82f13c9682b1d9e281eb1bc2ae3 Mon Sep 17 00:00:00 2001 From: humbletim Date: Mon, 24 Apr 2017 19:22:02 -0400 Subject: [PATCH 1/9] add getJointTranslations() methods --- interface/src/avatar/Avatar.cpp | 11 +++++++++++ interface/src/avatar/Avatar.h | 1 + libraries/avatars/src/AvatarData.cpp | 16 ++++++++++++++++ libraries/avatars/src/AvatarData.h | 1 + libraries/avatars/src/ScriptAvatarData.cpp | 7 +++++++ libraries/avatars/src/ScriptAvatarData.h | 1 + 6 files changed, 37 insertions(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 5b996a3cdf..f29efb8c32 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -929,6 +929,17 @@ QVector Avatar::getJointRotations() const { return jointRotations; } +QVector Avatar::getJointTranslations() const { + if (QThread::currentThread() != thread()) { + return AvatarData::getJointTranslations(); + } + QVector jointTranslations(_skeletonModel->getJointStateCount()); + for (int i = 0; i < _skeletonModel->getJointStateCount(); ++i) { + _skeletonModel->getJointTranslation(i, jointTranslations[i]); + } + return jointTranslations; +} + glm::quat Avatar::getJointRotation(int index) const { glm::quat rotation; _skeletonModel->getJointRotation(index, rotation); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 8c055885fd..14d1da530a 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -112,6 +112,7 @@ public: virtual QVector getJointRotations() const override; virtual glm::quat getJointRotation(int index) const override; + virtual QVector getJointTranslations() const override; virtual glm::vec3 getJointTranslation(int index) const override; virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9802630cf5..b805f68bed 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1391,6 +1391,22 @@ void AvatarData::setJointRotations(QVector jointRotations) { } } +QVector AvatarData::getJointTranslations() const { + if (QThread::currentThread() != thread()) { + QVector result; + QMetaObject::invokeMethod(const_cast(this), + "getJointTranslations", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QVector, result)); + return result; + } + QReadLocker readLock(&_jointDataLock); + QVector jointTranslations(_jointData.size()); + for (int i = 0; i < _jointData.size(); ++i) { + jointTranslations[i] = _jointData[i].translation; + } + return jointTranslations; +} + void AvatarData::setJointTranslations(QVector jointTranslations) { if (QThread::currentThread() != thread()) { QVector result; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 8319eb5249..e05bdce162 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -497,6 +497,7 @@ public: Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const; Q_INVOKABLE virtual QVector getJointRotations() const; + Q_INVOKABLE virtual QVector getJointTranslations() const; Q_INVOKABLE virtual void setJointRotations(QVector jointRotations); Q_INVOKABLE virtual void setJointTranslations(QVector jointTranslations); diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index f579eb9763..01d7f293d8 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -210,6 +210,13 @@ QVector ScriptAvatarData::getJointRotations() const { return QVector(); } } +QVector ScriptAvatarData::getJointTranslations() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getJointTranslations(); + } else { + return QVector(); + } +} bool ScriptAvatarData::isJointDataValid(const QString& name) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->isJointDataValid(name); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 683306e847..d763b6e97a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -106,6 +106,7 @@ public: Q_INVOKABLE glm::quat getJointRotation(const QString& name) const; Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const; Q_INVOKABLE QVector getJointRotations() const; + Q_INVOKABLE QVector getJointTranslations() const; Q_INVOKABLE bool isJointDataValid(const QString& name) const; Q_INVOKABLE int getJointIndex(const QString& name) const; Q_INVOKABLE QStringList getJointNames() const; From 3641ca38166a4a9d18dd6092410c3850d9ba2ff0 Mon Sep 17 00:00:00 2001 From: humbletim Date: Mon, 24 Apr 2017 19:24:54 -0400 Subject: [PATCH 2/9] add support for jointRotations/jointTranslations to ModelOverlay --- interface/src/ui/overlays/ModelOverlay.cpp | 101 +++++++++++++++++++++ interface/src/ui/overlays/ModelOverlay.h | 6 ++ 2 files changed, 107 insertions(+) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ccaa1d4fbc..e993166558 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -126,6 +126,55 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { QMetaObject::invokeMethod(_model.get(), "setTextures", Qt::AutoConnection, Q_ARG(const QVariantMap&, textureMap)); } + + // relative + auto jointTranslationsValue = properties["jointTranslations"]; + if (jointTranslationsValue.canConvert(QVariant::List)) { + const QVariantList& jointTranslations = jointTranslationsValue.toList(); + int translationCount = jointTranslations.size(); + int jointCount = _model->getJointStateCount(); + if (translationCount < jointCount) { + jointCount = translationCount; + } + for (int i=0; i < jointCount; i++) { + const auto& translationValue = jointTranslations[i]; + if (translationValue.isValid()) { + _model->setJointTranslation(i, true, vec3FromVariant(translationValue), 1.0f); + } + } + _updateModel = true; + } + + // relative + auto jointRotationsValue = properties["jointRotations"]; + if (jointRotationsValue.canConvert(QVariant::List)) { + const QVariantList& jointRotations = jointRotationsValue.toList(); + int rotationCount = jointRotations.size(); + int jointCount = _model->getJointStateCount(); + if (rotationCount < jointCount) { + jointCount = rotationCount; + } + for (int i=0; i < jointCount; i++) { + const auto& rotationValue = jointRotations[i]; + if (rotationValue.isValid()) { + _model->setJointRotation(i, true, quatFromVariant(rotationValue), 1.0f); + } + } + _updateModel = true; + } +} + +template +vectorType ModelOverlay::mapJoints(mapFunction function) const { + vectorType result; + if (_model && _model->isActive()) { + const int jointCount = _model->getJointStateCount(); + result.reserve(jointCount); + for (int i = 0; i < jointCount; i++) { + result << function(i); + } + } + return result; } QVariant ModelOverlay::getProperty(const QString& property) { @@ -150,6 +199,58 @@ QVariant ModelOverlay::getProperty(const QString& property) { } } + if (property == "jointNames") { + if (_model && _model->isActive()) { + // note: going through Rig because Model::getJointNames() (which proxies to FBXGeometry) was always empty + const RigPointer rig = _model->getRig(); + if (rig) { + return mapJoints([rig](int jointIndex) -> QString { + return rig->nameOfJoint(jointIndex); + }); + } + } + } + + // relative + if (property == "jointRotations") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::quat rotation; + _model->getJointRotation(jointIndex, rotation); + return quatToVariant(rotation); + }); + } + + // relative + if (property == "jointTranslations") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::vec3 translation; + _model->getJointTranslation(jointIndex, translation); + return vec3toVariant(translation); + }); + } + + // absolute + if (property == "jointOrientations") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::quat orientation; + _model->getJointRotationInWorldFrame(jointIndex, orientation); + return quatToVariant(orientation); + }); + } + + // absolute + if (property == "jointPositions") { + return mapJoints( + [this](int jointIndex) -> QVariant { + glm::vec3 position; + _model->getJointPositionInWorldFrame(jointIndex, position); + return vec3toVariant(position); + }); + } + return Volume3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index a3ddeed480..8afe9a20b6 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -41,6 +41,12 @@ public: void locationChanged(bool tellPhysics) override; +protected: + // helper to extract metadata from our Model's rigged joints + template using mapFunction = std::function; + template + vectorType mapJoints(mapFunction function) const; + private: ModelPointer _model; From 6f178ee1ec499bd419d62cdc3dfc35b5a05e41ce Mon Sep 17 00:00:00 2001 From: humbletim Date: Mon, 24 Apr 2017 19:28:03 -0400 Subject: [PATCH 3/9] initial checkin of doppleganger module and tablet app scripts --- scripts/system/app-doppleganger.js | 52 ++++++ scripts/system/doppleganger.js | 290 +++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 scripts/system/app-doppleganger.js create mode 100644 scripts/system/doppleganger.js diff --git a/scripts/system/app-doppleganger.js b/scripts/system/app-doppleganger.js new file mode 100644 index 0000000000..1d5eabdb77 --- /dev/null +++ b/scripts/system/app-doppleganger.js @@ -0,0 +1,52 @@ +"use strict"; + +// doppleganger-app.js +// +// Created by Timothy Dedischew on 04/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// This tablet app creates a mirrored projection of your avatar (ie: a "doppleganger") that you can walk around +// and inspect. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global */ + +var TABLET_APP_ICON = Script.resolvePath('Spiegel-lineart-black.svg'); +var TABLET_APP_NAME = 'mirror'; + +var EYE_TO_EYE = false; // whether to maintain the doppleganger's relative vertical positioning +var DEBUG = true; +var MIRRORED = true; // whether to mirror joints or simply transfer them as-is + +var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'); +var button = tablet.addButton({ + icon: TABLET_APP_ICON, + text: TABLET_APP_NAME +}); + +var DopplegangerClass = Script.require('./doppleganger.js#'+ new Date().getTime().toString(36)); + +var doppleganger = new DopplegangerClass({ + avatar: MyAvatar, + mirrored: MIRRORED, + debug: DEBUG, + eyeToEye: EYE_TO_EYE, +}); + +button.clicked.connect(function() { + print('click', doppleganger.active); + doppleganger.toggle(); + button.editProperties({ isActive: doppleganger.active }); +}); + +Script.scriptEnding.connect(function() { + try { + doppleganger.shutdown(); + } finally { + // we want to remove the button even if an error is thrown during shutdown + tablet.removeButton(button); + } +}); diff --git a/scripts/system/doppleganger.js b/scripts/system/doppleganger.js new file mode 100644 index 0000000000..a1df4e723f --- /dev/null +++ b/scripts/system/doppleganger.js @@ -0,0 +1,290 @@ +"use strict"; + +// doppleganger.js +// +// Created by Timothy Dedischew on 04/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// This tablet app creates a mirrored projection of your avatar (ie: a "doppleganger") that you can walk around +// and inspect. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global */ + +var USE_SCRIPT_UPDATE = false; // if this is true then Script.update will be used to update the doppleganger joints +var TARGET_FPS = 60; // when USE_SCRIPT_UPDATE is false, Script.setInterval will be used and target this FPS + +module.exports = Doppleganger; + +if (!Function.prototype.bind) { + // FIXME: this inline version is meant to be temporary, pending either a system-wide version being adopted + // or libraries/utils.js becoming a clean .require'able module + Function.prototype.bind = function(){var fn=this,s=[].slice,a=s.call(arguments),o=a.shift();return function(){return fn.apply(o,a.concat(s.call(arguments)))}}; +} + +function Doppleganger(options) { + this.avatar = options.avatar || MyAvatar; + this.mirrored = 'mirrored' in options ? options.mirrored : true; + this.debug = 'debug' in options ? options.debug : true; + this.eyeToEye = 'eyeToEye' in options ? options.eyeToEye : true; + + // instance properties + this.active = false; + this.uuid = null; + this.interval = null; + this.selectedJoint = null; + this.positionNeedsUpdate = true; +} + +Doppleganger.prototype = { + toggle: function() { + if (this.active) { + print('toggling off'); + this.off(); + this.active = false; + } else { + print('toggling on'); + this.on(); + this.active = true; + } + return this.active; + }, + syncJointName: 'LeftEye', + update: function() { + try { + if (!this.uuid) { + throw new Error('!this.uuid'); + } + var rotations = this.avatar.getJointRotations(); + var translations = this.avatar.getJointTranslations() || rotations.map(function(_, i) { + return this.avatar.getJointTranslation(i); + }.bind(this)); + + if (this.mirrored) { + var mirroredIndexes = this.mirroredIndexes; + var size = rotations.length; + var outRotations = new Array(size); + var outTranslations = new Array(size); + for (var i=0; i < size; i++) { + var index = mirroredIndexes[i] === false ? i : mirroredIndexes[i]; + var rot = rotations[index]; + var trans = translations[index]; + trans.x *= -1; + rot.y *= -1; + rot.z *= -1; + outRotations[i] = rot; + outTranslations[i] = trans; + } + rotations = outRotations; + translations = outTranslations; + } + Overlays.editOverlay(this.uuid, { + visible: true, + jointRotations: rotations, + jointTranslations: translations, + }); + + if (this.positionNeedsUpdate) { + this.positionNeedsUpdate = this.eyeToEye; // only continue updating if seeing eye-to-eye + this.fixVerticalPosition(); + } + if (this.debug) { + this._drawDebugOverlays(); + } + } catch(e) { + print('update ERROR: '+ e); + this.off(); + } + }, + on: function() { + if (this.uuid) { + print('doppleganger -- on() called but overlay model already exists', this.uuid); + return; + } + this.position = Vec3.sum(this.avatar.position, Quat.getFront(this.avatar.orientation)); + this.orientation = this.avatar.orientation; + this.skeletonModelURL = this.avatar.skeletonModelURL; + + this.jointNames = this.avatar.jointNames; + this.mirroredNames = this.jointNames.map(function(name, i) { + if (/Left/.test(name)) { + return name.replace('Left', 'Right'); + } + if (/Right/.test(name)) { + return name.replace('Right', 'Left'); + } + }); + + this.mirroredIndexes = this.mirroredNames.map(function(name) { + return name ? this.avatar.getJointIndex(name) : false; + }.bind(this)); + + this.uuid = Overlays.addOverlay('model', { + visible: false, + url: this.skeletonModelURL, + position: this.position, + rotation: this.orientation, + }); + + this._onModelReady(function() { + if (USE_SCRIPT_UPDATE) { + this.onUpdate = this.update; + Script.update.connect(this, 'onUpdate'); + print('doppleganger will be updated from Script.update'); + } else { + print('doppleganger will be updated using Script.setInterval at', TARGET_FPS +'fps'); + this.interval = Script.setInterval(this.update.bind(this), 1000 / TARGET_FPS); + } + }); + + print('doppleganger created; overlayID =', this.uuid); + + // trigger clean up (and stop updates) if the overlay gets deleted by any means + this.onDeletedOverlay = this._onDeletedOverlay; + Overlays.overlayDeleted.connect(this, 'onDeletedOverlay'); + + // FIXME: remove this hook after verifying joint modes between mirrored/non-mirrored + if (true) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + } + }, + + // execute callback only after the ModelOverlay has finished loading + _onModelReady: function(callback) { + const TIMEOUT_MS = 50; + var waited = 0; + var waitForJointNames = function() { + var names = Overlays.getProperty(this.uuid, 'jointNames'); + if (Array.isArray(names) && names.length) { + print('jointNames', names); + callback.call(this, names); + } else { + print(waited++, 'waiting for doppleganger jointNames...'); + Script.setTimeout(waitForJointNames, TIMEOUT_MS); + } + }.bind(this); + return Script.setTimeout(waitForJointNames, TIMEOUT_MS); + }, + shutdown: function() { + this.off(); + this.active = false; + }, + off: function() { + if (this.onUpdate) { + Script.update.disconnect(this, 'onUpdate'); + delete this.onUpdate; + } + if (this.onDeletedOverlay) { + Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay'); + delete this.onDeletedOverlay; + } + if (this.onMousePressEvent) { + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = undefined; + } + if (this.uuid) { + Overlays.deleteOverlay(this.uuid); + this.uuid = undefined; + } + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(function(o) { Overlays.deleteOverlay(o); }); + this.debugOverlayIDs = undefined; + } + }, + + // ModelOverlays & ModelEntities get positioned slightly differently than rigged Avatars + // in EYE_TO_EYE mode this helper takes an actual measurement and adjusts the doppleganger + // so it always sees "eye to eye" with the avatar + fixVerticalPosition: function() { + var byJointName = this.syncJointName || 'Hips'; + + var names = Overlays.getProperty(this.uuid, 'jointNames'), + positions = Overlays.getProperty(this.uuid, 'jointPositions'), + dopplePosition = Overlays.getProperty(this.uuid, 'position'), + doppleJointIndex = names.indexOf(byJointName), + doppleJointPosition = positions[doppleJointIndex]; + + var avatarPosition = this.avatar.position, + avatarJointIndex = this.avatar.getJointIndex(byJointName), + avatarJointPosition = this.avatar.getAbsoluteJointTranslationInObjectFrame(avatarJointIndex); + + var offset = Vec3.subtract(avatarJointPosition, doppleJointPosition); + + dopplePosition.y = avatarPosition.y + offset.y; + this.position = dopplePosition; + Overlays.editOverlay(this.uuid, { position: this.position }); + }, + + _onDeletedOverlay: function(uuid) { + print('onDeletedOverlay', uuid); + if (uuid === this.uuid) { + this.off(); + } + }, + + // DEBUG methods for verifying mirrored joint behaviors + _onMousePressEvent: function(evt) { + if (evt.isRightButton) { + this.mirrored = !this.mirrored; + } + if (!this.debug || !evt.isLeftButton) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.index = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.index]; + hit.mirroredJointName = this.mirroredNames[hit.index]; + this.selectedJoint = hit.jointName; + print('selected joint:', JSON.stringify(hit,0,2)); + }, + + _drawDebugOverlays: function() { + const COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; + const COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + + var id = this.uuid, + jointOrientations = Overlays.getProperty(id, 'jointOrientations'), + jointPositions = Overlays.getProperty(id, 'jointPositions'), + position = Overlays.getProperty(id, 'position'), + orientation = Overlays.getProperty(id, 'orientation'), + selectedIndex = this.jointNames.indexOf(this.selectedJoint); + + if (!this.debugOverlayIDs) { + // set up reference shapes per joint + this.debugOverlayIDs = jointOrientations.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: .1, + drawInFront: false, + text: this.jointNames[i], + solid: false, + alpha: .5, + }); + }.bind(this)); + } + + // group updates into { id: {props...}, ... } format + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i],//Vec3.sum(position, Vec3.multiplyQbyV(orientation, jointPositions[i])), + rotation: jointOrientations[i], + color: i === selectedIndex ? COLOR_SELECTED : COLOR_DEFAULT, + solid: i === selectedIndex, + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, +}; + + From 760113f9c911b41c45eb85d1039791d1109d365c Mon Sep 17 00:00:00 2001 From: humbletim Date: Tue, 25 Apr 2017 16:29:34 -0400 Subject: [PATCH 4/9] add doppleganger example/placeholder icons --- .../icons/tablet-icons/doppleganger-a.svg | 94 +++++++++++++++++++ .../icons/tablet-icons/doppleganger-i.svg | 94 +++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 interface/resources/icons/tablet-icons/doppleganger-a.svg create mode 100644 interface/resources/icons/tablet-icons/doppleganger-i.svg diff --git a/interface/resources/icons/tablet-icons/doppleganger-a.svg b/interface/resources/icons/tablet-icons/doppleganger-a.svg new file mode 100644 index 0000000000..100986647e --- /dev/null +++ b/interface/resources/icons/tablet-icons/doppleganger-a.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/doppleganger-i.svg b/interface/resources/icons/tablet-icons/doppleganger-i.svg new file mode 100644 index 0000000000..0c55e0e0c7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/doppleganger-i.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml \ No newline at end of file From a8339123998ec532d41146153876f860a1172f0d Mon Sep 17 00:00:00 2001 From: humbletim Date: Tue, 25 Apr 2017 16:30:05 -0400 Subject: [PATCH 5/9] cleanup and documentation --- scripts/system/app-doppleganger.js | 46 ++-- scripts/system/doppleganger.js | 384 +++++++++++++++++++---------- 2 files changed, 270 insertions(+), 160 deletions(-) diff --git a/scripts/system/app-doppleganger.js b/scripts/system/app-doppleganger.js index 1d5eabdb77..e60299b823 100644 --- a/scripts/system/app-doppleganger.js +++ b/scripts/system/app-doppleganger.js @@ -5,8 +5,8 @@ // Created by Timothy Dedischew on 04/21/2017. // Copyright 2017 High Fidelity, Inc. // -// This tablet app creates a mirrored projection of your avatar (ie: a "doppleganger") that you can walk around -// and inspect. +// This Client script creates can instance of a Doppleganger that can be toggled on/off via tablet button. +// (for more info see doppleganger.js) // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -14,39 +14,35 @@ /* global */ -var TABLET_APP_ICON = Script.resolvePath('Spiegel-lineart-black.svg'); -var TABLET_APP_NAME = 'mirror'; +var DopplegangerClass = Script.require('./doppleganger.js'); +// uncomment the next line to sync via Script.update (instead of Script.setInterval) +// DopplegangerClass.USE_SCRIPT_UPDATE = true; -var EYE_TO_EYE = false; // whether to maintain the doppleganger's relative vertical positioning -var DEBUG = true; -var MIRRORED = true; // whether to mirror joints or simply transfer them as-is +var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), + button = tablet.addButton({ + icon: "icons/tablet-icons/doppleganger-i.svg", + activeIcon: "icons/tablet-icons/doppleganger-a.svg", + text: 'MIRROR' + }); -var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'); -var button = tablet.addButton({ - icon: TABLET_APP_ICON, - text: TABLET_APP_NAME +Script.scriptEnding.connect(function() { + tablet.removeButton(button); }); -var DopplegangerClass = Script.require('./doppleganger.js#'+ new Date().getTime().toString(36)); - var doppleganger = new DopplegangerClass({ avatar: MyAvatar, - mirrored: MIRRORED, - debug: DEBUG, - eyeToEye: EYE_TO_EYE, + mirrored: true, + eyeToEye: true, + autoUpdate: true }); +Script.scriptEnding.connect(doppleganger, 'cleanup'); + +if (Settings.getValue('debug.doppleganger', false)) { + DopplegangerClass.addDebugControls(doppleganger); +} button.clicked.connect(function() { - print('click', doppleganger.active); doppleganger.toggle(); button.editProperties({ isActive: doppleganger.active }); }); -Script.scriptEnding.connect(function() { - try { - doppleganger.shutdown(); - } finally { - // we want to remove the button even if an error is thrown during shutdown - tablet.removeButton(button); - } -}); diff --git a/scripts/system/doppleganger.js b/scripts/system/doppleganger.js index a1df4e723f..878db98425 100644 --- a/scripts/system/doppleganger.js +++ b/scripts/system/doppleganger.js @@ -5,63 +5,79 @@ // Created by Timothy Dedischew on 04/21/2017. // Copyright 2017 High Fidelity, Inc. // -// This tablet app creates a mirrored projection of your avatar (ie: a "doppleganger") that you can walk around -// and inspect. -// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global */ - -var USE_SCRIPT_UPDATE = false; // if this is true then Script.update will be used to update the doppleganger joints -var TARGET_FPS = 60; // when USE_SCRIPT_UPDATE is false, Script.setInterval will be used and target this FPS +/* global module */ +// @module doppleganger +// +// This module contains the `Doppleganger` class implementation for creating an inspectable replica of +// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied +// over in an update thread, so that the model automatically mirrors the Avatar's joint movements. +// An Avatar can then for example walk around "themselves" and examine from the back, etc. +// +// This should be helpful for inspecting your own look and debugging avatars, etc. +// +// The doppleganger is created as an overlay so that others do not see it -- and this also allows for the +// highest possible update rate when keeping joint data in sync. module.exports = Doppleganger; -if (!Function.prototype.bind) { - // FIXME: this inline version is meant to be temporary, pending either a system-wide version being adopted - // or libraries/utils.js becoming a clean .require'able module - Function.prototype.bind = function(){var fn=this,s=[].slice,a=s.call(arguments),o=a.shift();return function(){return fn.apply(o,a.concat(s.call(arguments)))}}; -} +// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data +Doppleganger.USE_SCRIPT_UPDATE = false; +// @property {int} - the frame rate to target when using setInterval for joint updates +Doppleganger.TARGET_FPS = 60; + +// @class Doppleganger - Creates a new instance of a Doppleganger. +// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data. +// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints. +// @param {bool} [options.autoUpdate=true] - Automatically sync joint data. function Doppleganger(options) { + options = options || {}; this.avatar = options.avatar || MyAvatar; this.mirrored = 'mirrored' in options ? options.mirrored : true; - this.debug = 'debug' in options ? options.debug : true; - this.eyeToEye = 'eyeToEye' in options ? options.eyeToEye : true; + this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; - // instance properties - this.active = false; - this.uuid = null; - this.interval = null; - this.selectedJoint = null; - this.positionNeedsUpdate = true; + // @public + this.active = false; // whether doppleganger is currently being displayed/updated + this.uuid = null; // current doppleganger's Overlay id + this.ready = false; // whether the underlying ModelOverlay has finished loading + this.frame = 0; // current joint update frame } Doppleganger.prototype = { + // @public @method - toggles doppleganger on/off toggle: function() { if (this.active) { - print('toggling off'); + log('toggling off'); this.off(); this.active = false; } else { - print('toggling on'); + log('toggling on'); this.on(); this.active = true; } return this.active; }, - syncJointName: 'LeftEye', + + // @public @method - shutdown the dopplgeganger completely + cleanup: function() { + this.off(); + this.active = false; + }, + + // @public @method - synchronize the joint data between Avatar / doppleganger update: function() { + this.frame++; try { if (!this.uuid) { throw new Error('!this.uuid'); } + var rotations = this.avatar.getJointRotations(); - var translations = this.avatar.getJointTranslations() || rotations.map(function(_, i) { - return this.avatar.getJointTranslation(i); - }.bind(this)); + var translations = this.avatar.getJointTranslations(); if (this.mirrored) { var mirroredIndexes = this.mirroredIndexes; @@ -82,28 +98,29 @@ Doppleganger.prototype = { translations = outTranslations; } Overlays.editOverlay(this.uuid, { - visible: true, jointRotations: rotations, - jointTranslations: translations, + jointTranslations: translations }); - if (this.positionNeedsUpdate) { - this.positionNeedsUpdate = this.eyeToEye; // only continue updating if seeing eye-to-eye - this.fixVerticalPosition(); + // debug plumbing + if (this.$update) { + this.$update(); } - if (this.debug) { - this._drawDebugOverlays(); - } - } catch(e) { - print('update ERROR: '+ e); + } catch (e) { + log('update ERROR: '+ e); this.off(); } }, + + // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). on: function() { if (this.uuid) { - print('doppleganger -- on() called but overlay model already exists', this.uuid); + log('on() called but overlay model already exists', this.uuid); return; } + this.ready = false; + this.frame = 0; + this.position = Vec3.sum(this.avatar.position, Quat.getFront(this.avatar.orientation)); this.orientation = this.avatar.orientation; this.skeletonModelURL = this.avatar.skeletonModelURL; @@ -118,62 +135,43 @@ Doppleganger.prototype = { } }); + var avatar = this.avatar; this.mirroredIndexes = this.mirroredNames.map(function(name) { - return name ? this.avatar.getJointIndex(name) : false; - }.bind(this)); + return name ? avatar.getJointIndex(name) : false; + }); this.uuid = Overlays.addOverlay('model', { visible: false, url: this.skeletonModelURL, position: this.position, - rotation: this.orientation, + rotation: this.orientation }); - this._onModelReady(function() { - if (USE_SCRIPT_UPDATE) { - this.onUpdate = this.update; - Script.update.connect(this, 'onUpdate'); - print('doppleganger will be updated from Script.update'); - } else { - print('doppleganger will be updated using Script.setInterval at', TARGET_FPS +'fps'); - this.interval = Script.setInterval(this.update.bind(this), 1000 / TARGET_FPS); + this._onModelOverlayReady(bind(this, function() { + this.ready = true; + Overlays.editOverlay(this.uuid, { visible: true }); + this.syncVerticalPosition(); + log('ModelOverlay is ready; # joints == ' + Overlays.getProperty(this.uuid, 'jointNames').length); + if (this.autoUpdate) { + this._createUpdateThread(); } - }); + })); - print('doppleganger created; overlayID =', this.uuid); + log('doppleganger created; overlayID =', this.uuid); - // trigger clean up (and stop updates) if the overlay gets deleted by any means + // trigger clean up (and stop updates) if the overlay gets deleted this.onDeletedOverlay = this._onDeletedOverlay; Overlays.overlayDeleted.connect(this, 'onDeletedOverlay'); - // FIXME: remove this hook after verifying joint modes between mirrored/non-mirrored - if (true) { - this.onMousePressEvent = this._onMousePressEvent; - Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + // debug plumbing + if (this.$on) { + this.$on(); } }, - // execute callback only after the ModelOverlay has finished loading - _onModelReady: function(callback) { - const TIMEOUT_MS = 50; - var waited = 0; - var waitForJointNames = function() { - var names = Overlays.getProperty(this.uuid, 'jointNames'); - if (Array.isArray(names) && names.length) { - print('jointNames', names); - callback.call(this, names); - } else { - print(waited++, 'waiting for doppleganger jointNames...'); - Script.setTimeout(waitForJointNames, TIMEOUT_MS); - } - }.bind(this); - return Script.setTimeout(waitForJointNames, TIMEOUT_MS); - }, - shutdown: function() { - this.off(); - this.active = false; - }, + // @public @method - hide the doppleganger off: function() { + this.ready = false; if (this.onUpdate) { Script.update.disconnect(this, 'onUpdate'); delete this.onUpdate; @@ -182,30 +180,27 @@ Doppleganger.prototype = { Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay'); delete this.onDeletedOverlay; } - if (this.onMousePressEvent) { - Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); - delete this.onMousePressEvent; - } - if (this.interval) { - Script.clearInterval(this.interval); - this.interval = undefined; + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = undefined; } if (this.uuid) { Overlays.deleteOverlay(this.uuid); this.uuid = undefined; } - if (this.debugOverlayIDs) { - this.debugOverlayIDs.forEach(function(o) { Overlays.deleteOverlay(o); }); - this.debugOverlayIDs = undefined; + // debug plumbing + if (this.$off) { + this.$off(); } }, - // ModelOverlays & ModelEntities get positioned slightly differently than rigged Avatars - // in EYE_TO_EYE mode this helper takes an actual measurement and adjusts the doppleganger - // so it always sees "eye to eye" with the avatar - fixVerticalPosition: function() { - var byJointName = this.syncJointName || 'Hips'; - + // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar. + // @param {String} [byJointName=Hips] - the reference joint that will be used to vertically match positions with Avatar + // @note This method attempts to make a direct measurement and then calculate where the doppleganger needs to be + // in order to line-up vertically with the Avatar. Otherwise, animations such as "away" mode can + // result in the doppleganger floating above or below ground level. + syncVerticalPosition: function s(byJointName) { + byJointName = byJointName || 'Hips'; var names = Overlays.getProperty(this.uuid, 'jointNames'), positions = Overlays.getProperty(this.uuid, 'jointPositions'), dopplePosition = Overlays.getProperty(this.uuid, 'position'), @@ -214,77 +209,196 @@ Doppleganger.prototype = { var avatarPosition = this.avatar.position, avatarJointIndex = this.avatar.getJointIndex(byJointName), - avatarJointPosition = this.avatar.getAbsoluteJointTranslationInObjectFrame(avatarJointIndex); + avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex); - var offset = Vec3.subtract(avatarJointPosition, doppleJointPosition); - - dopplePosition.y = avatarPosition.y + offset.y; + var offset = avatarJointPosition.y - doppleJointPosition.y; + log('adjusting for offset', offset); + dopplePosition.y = avatarPosition.y + offset; this.position = dopplePosition; Overlays.editOverlay(this.uuid, { position: this.position }); }, + // @private @method - signal handler for Overlays.overlayDeleted _onDeletedOverlay: function(uuid) { - print('onDeletedOverlay', uuid); + log('onDeletedOverlay', uuid); if (uuid === this.uuid) { this.off(); } }, - // DEBUG methods for verifying mirrored joint behaviors - _onMousePressEvent: function(evt) { - if (evt.isRightButton) { - this.mirrored = !this.mirrored; - } - if (!this.debug || !evt.isLeftButton) { + // @private @method - creates the update thread to synchronize joint data + _createUpdateThread: function() { + if (!this.autoUpdate) { + log('options.autoUpdate == false -- call .update() manually to sync joint data'); return; } - var ray = Camera.computePickRay(evt.x, evt.y), - hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); - - hit.index = this.debugOverlayIDs.indexOf(hit.overlayID); - hit.jointName = this.jointNames[hit.index]; - hit.mirroredJointName = this.mirroredNames[hit.index]; - this.selectedJoint = hit.jointName; - print('selected joint:', JSON.stringify(hit,0,2)); + if (Doppleganger.USE_SCRIPT_UPDATE) { + log('creating Script.update thread'); + this.onUpdate = this.update; + Script.update.connect(this, 'onUpdate'); + } else { + log('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); + var timeout = 1000 / Doppleganger.TARGET_FPS; + this._interval = Script.setInterval(bind(this, 'update'), timeout); + } }, - _drawDebugOverlays: function() { - const COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; - const COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + // @private @method - Invokes a callback once the ModelOverlay is fully-initialized. + // @param {Function} callback + // @note This is needed because sometimes it takes a few frames for the underlying model + // to become loaded even when already cached locally. + _onModelOverlayReady: function(callback) { + var RECHECK_MS = 50, MAX_WAIT_MS = 10000; + var id = this.uuid, + watchdogTimer = null, + boundCallback = bind(this, callback); + + function waitForJointNames() { + if (!watchdogTimer) { + log('stopping waitForJointNames...'); + return; + } + var names = Overlays.getProperty(id, 'jointNames'); + if (Array.isArray(names) && names.length) { + log('ModelOverlay ready -- jointNames:', names); + boundCallback(names); + Script.clearTimeout(watchdogTimer); + } else { + return Script.setTimeout(waitForJointNames, RECHECK_MS); + } + } + watchdogTimer = Script.setTimeout(function() { + watchdogTimer = null; + }, MAX_WAIT_MS); + waitForJointNames(); + } +}; + +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +function bind(thiz, method) { + method = thiz[method] || method; + return function() { + return method.apply(thiz, arguments); + }; +} + +// @function - debug logging +function log() { + print('doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new Doppleganger(); +// Doppleganger.addDebugControls(doppleganger); +Doppleganger.addDebugControls = function(doppleganger) { + var onMousePressEvent, + debugOverlayIDs, + selectedJointName; + + if ('$update' in doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + + function $on() { + onMousePressEvent = _onMousePressEvent; + Controller.mousePressEvent.connect(this, _onMousePressEvent); + } + + function createOverlays(jointNames) { + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + } + + function cleanupOverlays() { + if (debugOverlayIDs) { + debugOverlayIDs.forEach(Overlays.deleteOverlay); + debugOverlayIDs = undefined; + } + } + + function $off() { + if (onMousePressEvent) { + Controller.mousePressEvent.disconnect(this, onMousePressEvent); + onMousePressEvent = undefined; + } + cleanupOverlays(); + } + + function $update() { + var COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; + var COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; var id = this.uuid, jointOrientations = Overlays.getProperty(id, 'jointOrientations'), jointPositions = Overlays.getProperty(id, 'jointPositions'), - position = Overlays.getProperty(id, 'position'), - orientation = Overlays.getProperty(id, 'orientation'), - selectedIndex = this.jointNames.indexOf(this.selectedJoint); + selectedIndex = this.jointNames.indexOf(selectedJointName); - if (!this.debugOverlayIDs) { - // set up reference shapes per joint - this.debugOverlayIDs = jointOrientations.map(function(name, i) { - return Overlays.addOverlay('shape', { - shape: 'Icosahedron', - scale: .1, - drawInFront: false, - text: this.jointNames[i], - solid: false, - alpha: .5, - }); - }.bind(this)); + if (!debugOverlayIDs) { + debugOverlayIDs = createOverlays(this.jointNames); } - // group updates into { id: {props...}, ... } format - var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = debugOverlayIDs.reduce(function(updates, id, i) { updates[id] = { - position: jointPositions[i],//Vec3.sum(position, Vec3.multiplyQbyV(orientation, jointPositions[i])), + position: jointPositions[i], rotation: jointOrientations[i], color: i === selectedIndex ? COLOR_SELECTED : COLOR_DEFAULT, - solid: i === selectedIndex, + solid: i === selectedIndex }; return updates; }, {}); Overlays.editOverlays(updatedOverlays); - }, + } + + function _onMousePressEvent(evt) { + if (evt.isRightButton) { + if (evt.isShifted) { + if (this.$update) { + // toggle debug overlays off + cleanupOverlays(); + delete this.$update; + } else { + this.$update = $update; + } + } else { + this.mirrored = !this.mirrored; + } + return; + } + if (!evt.isLeftButton || !debugOverlayIDs) { + return; + } + + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, debugOverlayIDs); + + hit.jointIndex = debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + hit.mirroredJointName = this.mirroredNames[hit.jointIndex]; + selectedJointName = hit.jointName; + log('selected joint:', JSON.stringify(hit, 0, 2)); + } + + doppleganger.$on = $on; + doppleganger.$off = $off; + doppleganger.$update = $update; + + return doppleganger; }; - - From 84c8b2945aff9ff130520addddcbd18a4f384041 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 26 Apr 2017 01:58:15 -0400 Subject: [PATCH 6/9] detect and update doppleganger when user changes skeletonModelURLs; cleanup --- scripts/system/app-doppleganger.js | 26 +++-- scripts/system/doppleganger.js | 176 ++++++++++++++++++++--------- 2 files changed, 142 insertions(+), 60 deletions(-) diff --git a/scripts/system/app-doppleganger.js b/scripts/system/app-doppleganger.js index e60299b823..340dee5ff0 100644 --- a/scripts/system/app-doppleganger.js +++ b/scripts/system/app-doppleganger.js @@ -5,7 +5,7 @@ // Created by Timothy Dedischew on 04/21/2017. // Copyright 2017 High Fidelity, Inc. // -// This Client script creates can instance of a Doppleganger that can be toggled on/off via tablet button. +// This Client script creates an instance of a Doppleganger that can be toggled on/off via tablet button. // (for more info see doppleganger.js) // // Distributed under the Apache License, Version 2.0. @@ -27,22 +27,30 @@ var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), Script.scriptEnding.connect(function() { tablet.removeButton(button); + button = null; }); var doppleganger = new DopplegangerClass({ avatar: MyAvatar, mirrored: true, - eyeToEye: true, autoUpdate: true }); -Script.scriptEnding.connect(doppleganger, 'cleanup'); +Script.scriptEnding.connect(doppleganger, 'stop'); + +doppleganger.activeChanged.connect(function(active) { + if (button) { + button.editProperties({ isActive: active }); + } +}); + +doppleganger.modelOverlayLoaded.connect(function(error, result) { + if (doppleganger.active && error) { + Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL); + } +}); + +button.clicked.connect(doppleganger, 'toggle'); if (Settings.getValue('debug.doppleganger', false)) { DopplegangerClass.addDebugControls(doppleganger); } - -button.clicked.connect(function() { - doppleganger.toggle(); - button.editProperties({ isActive: doppleganger.active }); -}); - diff --git a/scripts/system/doppleganger.js b/scripts/system/doppleganger.js index 878db98425..18567ad89a 100644 --- a/scripts/system/doppleganger.js +++ b/scripts/system/doppleganger.js @@ -43,8 +43,12 @@ function Doppleganger(options) { // @public this.active = false; // whether doppleganger is currently being displayed/updated this.uuid = null; // current doppleganger's Overlay id - this.ready = false; // whether the underlying ModelOverlay has finished loading this.frame = 0; // current joint update frame + + // @signal - emitted when .active state changes + this.activeChanged = signal(function(active) {}); + // @signal - emitted once model overlay is either loaded or times out + this.modelOverlayLoaded = signal(function(error, result){}); } Doppleganger.prototype = { @@ -52,20 +56,26 @@ Doppleganger.prototype = { toggle: function() { if (this.active) { log('toggling off'); - this.off(); - this.active = false; + this.stop(); } else { log('toggling on'); - this.on(); - this.active = true; + this.start(); } return this.active; }, - // @public @method - shutdown the dopplgeganger completely - cleanup: function() { - this.off(); - this.active = false; + // @public @method - re-initialize model if Avatar changed skeletonModelURLs + refreshAvatarModel: function(forceRefresh) { + if (forceRefresh || (this.active && this.skeletonModelURL !== this.avatar.skeletonModelURL)) { + var currentState = { position: this.position, orientation: this.orientation }; + this.stop(); + // turn back on with next script update tick + Script.setTimeout(bind(this, function() { + log('recreating doppleganger with latest model:', this.avatar.skeletonModelURL); + this.start(currentState); + }), 0); + return true; + } }, // @public @method - synchronize the joint data between Avatar / doppleganger @@ -76,16 +86,38 @@ Doppleganger.prototype = { throw new Error('!this.uuid'); } + if (this.avatar.skeletonModelURL !== this.skeletonModelURL) { + return this.refreshAvatarModel(); + } + var rotations = this.avatar.getJointRotations(); var translations = this.avatar.getJointTranslations(); + var size = rotations.length; + + // note: this mismatch can happen when the avatar's model is actively changing + if (size !== translations.length || + (this._lastRotationLength && size !== this._lastRotationLength)) { + log('lengths differ', size, translations.length, this._lastRotationLength); + this._lastRotationLength = 0; + this.stop(); + this.skeletonModelURL = null; + // wait a second before restarting + Script.setTimeout(bind(this, function() { + this.refreshAvatarModel(true); + }), 1000); + return; + } + this._lastRotationLength = size; if (this.mirrored) { var mirroredIndexes = this.mirroredIndexes; - var size = rotations.length; var outRotations = new Array(size); var outTranslations = new Array(size); for (var i=0; i < size; i++) { - var index = mirroredIndexes[i] === false ? i : mirroredIndexes[i]; + var index = mirroredIndexes[i]; + if (index < 0 || index === false) { + index = i; + } var rot = rotations[index]; var trans = translations[index]; trans.x *= -1; @@ -107,22 +139,23 @@ Doppleganger.prototype = { this.$update(); } } catch (e) { - log('update ERROR: '+ e); - this.off(); + log('.update error: '+ e, index); + this.stop(); } }, // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). - on: function() { + start: function(transform) { if (this.uuid) { log('on() called but overlay model already exists', this.uuid); return; } - this.ready = false; + transform = transform || {}; + this.activeChanged(this.active = true); this.frame = 0; - this.position = Vec3.sum(this.avatar.position, Quat.getFront(this.avatar.orientation)); - this.orientation = this.avatar.orientation; + this.position = transform.position || Vec3.sum(this.avatar.position, Quat.getForward(this.avatar.orientation)); + this.orientation = transform.orientation || this.avatar.orientation; this.skeletonModelURL = this.avatar.skeletonModelURL; this.jointNames = this.avatar.jointNames; @@ -147,15 +180,21 @@ Doppleganger.prototype = { rotation: this.orientation }); - this._onModelOverlayReady(bind(this, function() { - this.ready = true; + this._waitForModel(); + this.onModelOverlayLoaded = function(error, result) { + if (error || this.uuid !== result.uuid) { + return; + } Overlays.editOverlay(this.uuid, { visible: true }); - this.syncVerticalPosition(); - log('ModelOverlay is ready; # joints == ' + Overlays.getProperty(this.uuid, 'jointNames').length); + if (!transform.position) { + this.syncVerticalPosition(); + } + log('ModelOverlay is ready; # joints == ' + result.jointNames.length); if (this.autoUpdate) { this._createUpdateThread(); } - })); + }; + this.modelOverlayLoaded.connect(this, 'onModelOverlayLoaded'); log('doppleganger created; overlayID =', this.uuid); @@ -163,34 +202,47 @@ Doppleganger.prototype = { this.onDeletedOverlay = this._onDeletedOverlay; Overlays.overlayDeleted.connect(this, 'onDeletedOverlay'); + if ('onLoadComplete' in this.avatar) { + // restart the doppleganger if Avatar loads a different model URL + this.onLoadComplete = this.refreshAvatarModel; + this.avatar.onLoadComplete.connect(this, 'onLoadComplete'); + } + // debug plumbing - if (this.$on) { - this.$on(); + if (this.$start) { + this.$start(); } }, // @public @method - hide the doppleganger - off: function() { - this.ready = false; + stop: function() { if (this.onUpdate) { Script.update.disconnect(this, 'onUpdate'); delete this.onUpdate; } - if (this.onDeletedOverlay) { - Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay'); - delete this.onDeletedOverlay; - } if (this._interval) { Script.clearInterval(this._interval); this._interval = undefined; } + if (this.onDeletedOverlay) { + Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay'); + delete this.onDeletedOverlay; + } + if (this.onLoadComplete) { + this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete'); + delete this.onLoadComplete; + } + if (this.onModelOverlayLoaded) { + this.modelOverlayLoaded.disconnect(this, 'onModelOverlayLoaded'); + } if (this.uuid) { Overlays.deleteOverlay(this.uuid); this.uuid = undefined; } + this.activeChanged(this.active = false); // debug plumbing - if (this.$off) { - this.$off(); + if (this.$stop) { + this.$stop(); } }, @@ -220,9 +272,9 @@ Doppleganger.prototype = { // @private @method - signal handler for Overlays.overlayDeleted _onDeletedOverlay: function(uuid) { - log('onDeletedOverlay', uuid); if (uuid === this.uuid) { - this.off(); + log('onDeletedOverlay', uuid); + this.stop(); } }, @@ -243,34 +295,32 @@ Doppleganger.prototype = { } }, - // @private @method - Invokes a callback once the ModelOverlay is fully-initialized. - // @param {Function} callback + // @private @method - waits for model to load and handles timeouts // @note This is needed because sometimes it takes a few frames for the underlying model // to become loaded even when already cached locally. - _onModelOverlayReady: function(callback) { + _waitForModel: function(callback) { var RECHECK_MS = 50, MAX_WAIT_MS = 10000; var id = this.uuid, - watchdogTimer = null, - boundCallback = bind(this, callback); + watchdogTimer = null; function waitForJointNames() { if (!watchdogTimer) { - log('stopping waitForJointNames...'); - return; + log('timeout waiting for ModelOverlay jointNames'); + Script.clearInterval(this._interval); + this._interval = null; + return this.modelOverlayLoaded(new Error('could not retrieve jointNames'), null); } var names = Overlays.getProperty(id, 'jointNames'); if (Array.isArray(names) && names.length) { - log('ModelOverlay ready -- jointNames:', names); - boundCallback(names); - Script.clearTimeout(watchdogTimer); - } else { - return Script.setTimeout(waitForJointNames, RECHECK_MS); + Script.clearInterval(this._interval); + this._interval = null; + return this.modelOverlayLoaded(null, { uuid: id, jointNames: names }); } } watchdogTimer = Script.setTimeout(function() { watchdogTimer = null; }, MAX_WAIT_MS); - waitForJointNames(); + this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS); } }; @@ -284,6 +334,27 @@ function bind(thiz, method) { }; } +// @function - Qt signal polyfill +function signal(template) { + var callbacks = []; + return Object.defineProperties(function() { + var args = [].slice.call(arguments); + callbacks.forEach(function(obj) { + obj.handler.apply(obj.scope, args); + }); + }, { + connect: { value: function(scope, handler) { + callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); + }}, + disconnect: { value: function(scope, handler) { + var match = {scope: scope, handler: scope[handler] || handler || scope}; + callbacks = callbacks.filter(function(obj) { + return !(obj.scope === match.scope && obj.handler === match.handler); + }); + }} + }); +} + // @function - debug logging function log() { print('doppleganger | ' + [].slice.call(arguments).join(' ')); @@ -310,7 +381,7 @@ Doppleganger.addDebugControls = function(doppleganger) { throw new Error('only one set of debug controls can be added per doppleganger'); } - function $on() { + function $start() { onMousePressEvent = _onMousePressEvent; Controller.mousePressEvent.connect(this, _onMousePressEvent); } @@ -333,7 +404,7 @@ Doppleganger.addDebugControls = function(doppleganger) { } } - function $off() { + function $stop() { if (onMousePressEvent) { Controller.mousePressEvent.disconnect(this, onMousePressEvent); onMousePressEvent = undefined; @@ -390,14 +461,17 @@ Doppleganger.addDebugControls = function(doppleganger) { hit = Overlays.findRayIntersection(ray, true, debugOverlayIDs); hit.jointIndex = debugOverlayIDs.indexOf(hit.overlayID); + if (hit.jointIndex < 0) { + return; + } hit.jointName = this.jointNames[hit.jointIndex]; hit.mirroredJointName = this.mirroredNames[hit.jointIndex]; selectedJointName = hit.jointName; log('selected joint:', JSON.stringify(hit, 0, 2)); } - doppleganger.$on = $on; - doppleganger.$off = $off; + doppleganger.$start = $start; + doppleganger.$stop = $stop; doppleganger.$update = $update; return doppleganger; From 060d5aa3cb47cd1d97ff9a1d7b4d818a51e809f8 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 26 Apr 2017 17:49:14 -0400 Subject: [PATCH 7/9] add "Resource" API global --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 9635142d1a..b4d88777f2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { "Quat": false, "Rates": false, "Recording": false, + "Resource": false, "Reticle": false, "Scene": false, "Script": false, From cabeced66e49548af3472da11318f7ddfe6cc951 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 26 Apr 2017 17:51:21 -0400 Subject: [PATCH 8/9] * changes per feedback and testing * add support for UserActivityLogger.logAction (commented-out, pending #10130) * autodisable doppleganger when avatar changes model * autodisable doppleganger when user moves to a different domain * use ModelCache.prefetch for detecting model load status * expose new signals to help keep activity logging and debugging decoupled --- scripts/system/app-doppleganger.js | 56 +++- scripts/system/doppleganger.js | 420 +++++++++++++++-------------- 2 files changed, 265 insertions(+), 211 deletions(-) diff --git a/scripts/system/app-doppleganger.js b/scripts/system/app-doppleganger.js index 340dee5ff0..9fdcc6fa03 100644 --- a/scripts/system/app-doppleganger.js +++ b/scripts/system/app-doppleganger.js @@ -1,5 +1,3 @@ -"use strict"; - // doppleganger-app.js // // Created by Timothy Dedischew on 04/21/2017. @@ -12,11 +10,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global */ - var DopplegangerClass = Script.require('./doppleganger.js'); -// uncomment the next line to sync via Script.update (instead of Script.setInterval) -// DopplegangerClass.USE_SCRIPT_UPDATE = true; var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), button = tablet.addButton({ @@ -35,22 +29,66 @@ var doppleganger = new DopplegangerClass({ mirrored: true, autoUpdate: true }); + +// hide the doppleganger if this client script is unloaded Script.scriptEnding.connect(doppleganger, 'stop'); -doppleganger.activeChanged.connect(function(active) { +// hide the doppleganger if the user switches domains (which might place them arbitrarily far away in world space) +function onDomainChanged() { + if (doppleganger.active) { + doppleganger.stop('domain_changed'); + } +} +Window.domainChanged.connect(onDomainChanged); +Window.domainConnectionRefused.connect(onDomainChanged); +Script.scriptEnding.connect(function() { + Window.domainChanged.disconnect(onDomainChanged); + Window.domainConnectionRefused.disconnect(onDomainChanged); +}); + +// toggle on/off via tablet button +button.clicked.connect(doppleganger, 'toggle'); + +// highlight tablet button based on current doppleganger state +doppleganger.activeChanged.connect(function(active, reason) { if (button) { button.editProperties({ isActive: active }); + print('doppleganger.activeChanged', active, reason); } }); +// alert the user if there was an error applying their skeletonModelURL doppleganger.modelOverlayLoaded.connect(function(error, result) { if (doppleganger.active && error) { Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL); } }); -button.clicked.connect(doppleganger, 'toggle'); - +// add debug indicators, but only if the user has configured the settings value if (Settings.getValue('debug.doppleganger', false)) { DopplegangerClass.addDebugControls(doppleganger); } + +// TODO: uncomment after PR #10130 is merged (which provides the needed .logAction method) +/* +UserActivityLogger.logAction('doppleganger_app_load'); + +// Script.scriptEnding.connect(function() { +// UserActivityLogger.logAction('doppleganger_app_unload'); +// }); + +doppleganger.activeChanged.connect(function(active, reason) { + if (active) { + UserActivityLogger.logAction('doppleganger_enable'); + } else { + if (reason === 'stop') { + // user intentionally toggled the doppleganger + UserActivityLogger.logAction('doppleganger_disable'); + } else { + print('doppleganger stopped:', reason); + UserActivityLogger.logAction('doppleganger_autodisable', { reason: reason }); + } + } +}); + +*/ diff --git a/scripts/system/doppleganger.js b/scripts/system/doppleganger.js index 18567ad89a..271a9a67c5 100644 --- a/scripts/system/doppleganger.js +++ b/scripts/system/doppleganger.js @@ -30,6 +30,24 @@ Doppleganger.USE_SCRIPT_UPDATE = false; // @property {int} - the frame rate to target when using setInterval for joint updates Doppleganger.TARGET_FPS = 60; +// @property {int} - the maximum time in seconds to wait for the model overlay to finish loading +Doppleganger.MAX_WAIT_SECS = 10; + +// @function - derive mirrored joint names from a list of regular joint names +// @param {Array} - list of joint names to mirror +// @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`) +Doppleganger.getMirroredJointNames = function(jointNames) { + return jointNames.map(function(name, i) { + if (/Left/.test(name)) { + return name.replace('Left', 'Right'); + } + if (/Right/.test(name)) { + return name.replace('Right', 'Left'); + } + return undefined; + }); +}; + // @class Doppleganger - Creates a new instance of a Doppleganger. // @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data. // @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints. @@ -41,14 +59,16 @@ function Doppleganger(options) { this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; // @public - this.active = false; // whether doppleganger is currently being displayed/updated - this.uuid = null; // current doppleganger's Overlay id - this.frame = 0; // current joint update frame + this.active = false; // whether doppleganger is currently being displayed/updated + this.overlayID = null; // current doppleganger's Overlay id + this.frame = 0; // current joint update frame // @signal - emitted when .active state changes - this.activeChanged = signal(function(active) {}); - // @signal - emitted once model overlay is either loaded or times out + this.activeChanged = signal(function(active, reason) {}); + // @signal - emitted once model overlay is either loaded or errors out this.modelOverlayLoaded = signal(function(error, result){}); + // @signal - emitted each time the model overlay's joint data has been synchronized + this.jointsUpdated = signal(function(overlayID){}); } Doppleganger.prototype = { @@ -64,30 +84,16 @@ Doppleganger.prototype = { return this.active; }, - // @public @method - re-initialize model if Avatar changed skeletonModelURLs - refreshAvatarModel: function(forceRefresh) { - if (forceRefresh || (this.active && this.skeletonModelURL !== this.avatar.skeletonModelURL)) { - var currentState = { position: this.position, orientation: this.orientation }; - this.stop(); - // turn back on with next script update tick - Script.setTimeout(bind(this, function() { - log('recreating doppleganger with latest model:', this.avatar.skeletonModelURL); - this.start(currentState); - }), 0); - return true; - } - }, - // @public @method - synchronize the joint data between Avatar / doppleganger update: function() { this.frame++; try { - if (!this.uuid) { - throw new Error('!this.uuid'); + if (!this.overlayID) { + throw new Error('!this.overlayID'); } if (this.avatar.skeletonModelURL !== this.skeletonModelURL) { - return this.refreshAvatarModel(); + return this.stop('avatar_changed'); } var rotations = this.avatar.getJointRotations(); @@ -96,18 +102,12 @@ Doppleganger.prototype = { // note: this mismatch can happen when the avatar's model is actively changing if (size !== translations.length || - (this._lastRotationLength && size !== this._lastRotationLength)) { - log('lengths differ', size, translations.length, this._lastRotationLength); - this._lastRotationLength = 0; - this.stop(); - this.skeletonModelURL = null; - // wait a second before restarting - Script.setTimeout(bind(this, function() { - this.refreshAvatarModel(true); - }), 1000); + (this.jointStateCount && size !== this.jointStateCount)) { + log('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount); + this.stop('avatar_changed_joints'); return; } - this._lastRotationLength = size; + this.jointStateCount = size; if (this.mirrored) { var mirroredIndexes = this.mirroredIndexes; @@ -129,93 +129,94 @@ Doppleganger.prototype = { rotations = outRotations; translations = outTranslations; } - Overlays.editOverlay(this.uuid, { + Overlays.editOverlay(this.overlayID, { jointRotations: rotations, jointTranslations: translations }); - // debug plumbing - if (this.$update) { - this.$update(); - } + this.jointsUpdated(this.overlayID); } catch (e) { log('.update error: '+ e, index); - this.stop(); + this.stop('update_error'); } }, // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). - start: function(transform) { - if (this.uuid) { - log('on() called but overlay model already exists', this.uuid); + // @param {vec3} [options.position=(in front of avatar)] - starting position + // @param {quat} [options.orientation=avatar.orientation] - starting orientation + start: function(options) { + options = options || {}; + if (this.overlayID) { + log('start() called but overlay model already exists', this.overlayID); return; } - transform = transform || {}; - this.activeChanged(this.active = true); - this.frame = 0; - - this.position = transform.position || Vec3.sum(this.avatar.position, Quat.getForward(this.avatar.orientation)); - this.orientation = transform.orientation || this.avatar.orientation; - this.skeletonModelURL = this.avatar.skeletonModelURL; - - this.jointNames = this.avatar.jointNames; - this.mirroredNames = this.jointNames.map(function(name, i) { - if (/Left/.test(name)) { - return name.replace('Left', 'Right'); - } - if (/Right/.test(name)) { - return name.replace('Right', 'Left'); - } - }); - var avatar = this.avatar; + if (!avatar.jointNames.length) { + return this.stop('joints_unavailable'); + } + + this.frame = 0; + this.position = options.position || Vec3.sum(avatar.position, Quat.getForward(avatar.orientation)); + this.orientation = options.orientation || avatar.orientation; + this.skeletonModelURL = avatar.skeletonModelURL; + this.jointStateCount = 0; + this.jointNames = avatar.jointNames; + this.mirroredNames = Doppleganger.getMirroredJointNames(this.jointNames); this.mirroredIndexes = this.mirroredNames.map(function(name) { return name ? avatar.getJointIndex(name) : false; }); - this.uuid = Overlays.addOverlay('model', { + this.overlayID = Overlays.addOverlay('model', { visible: false, url: this.skeletonModelURL, position: this.position, rotation: this.orientation }); - this._waitForModel(); this.onModelOverlayLoaded = function(error, result) { - if (error || this.uuid !== result.uuid) { - return; - } - Overlays.editOverlay(this.uuid, { visible: true }); - if (!transform.position) { - this.syncVerticalPosition(); + if (error) { + return this.stop(error); } log('ModelOverlay is ready; # joints == ' + result.jointNames.length); + Overlays.editOverlay(this.overlayID, { visible: true }); + if (!options.position) { + this.syncVerticalPosition(); + } if (this.autoUpdate) { this._createUpdateThread(); } }; this.modelOverlayLoaded.connect(this, 'onModelOverlayLoaded'); - log('doppleganger created; overlayID =', this.uuid); + log('doppleganger created; overlayID =', this.overlayID); // trigger clean up (and stop updates) if the overlay gets deleted - this.onDeletedOverlay = this._onDeletedOverlay; + this.onDeletedOverlay = function(uuid) { + if (uuid === this.overlayID) { + log('onDeletedOverlay', uuid); + this.stop('overlay_deleted'); + } + }; Overlays.overlayDeleted.connect(this, 'onDeletedOverlay'); - if ('onLoadComplete' in this.avatar) { - // restart the doppleganger if Avatar loads a different model URL - this.onLoadComplete = this.refreshAvatarModel; - this.avatar.onLoadComplete.connect(this, 'onLoadComplete'); + if ('onLoadComplete' in avatar) { + // stop the current doppleganger if Avatar loads a different model URL + this.onLoadComplete = function() { + if (avatar.skeletonModelURL !== this.skeletonModelURL) { + this.stop('avatar_changed_load'); + } + }; + avatar.onLoadComplete.connect(this, 'onLoadComplete'); } - // debug plumbing - if (this.$start) { - this.$start(); - } + this.activeChanged(this.active = true, 'start'); + this._waitForModel(ModelCache.prefetch(this.skeletonModelURL)); }, // @public @method - hide the doppleganger - stop: function() { + // @param {String} [reason=stop] - the reason stop was called + stop: function(reason) { + reason = reason || 'stop'; if (this.onUpdate) { Script.update.disconnect(this, 'onUpdate'); delete this.onUpdate; @@ -235,27 +236,24 @@ Doppleganger.prototype = { if (this.onModelOverlayLoaded) { this.modelOverlayLoaded.disconnect(this, 'onModelOverlayLoaded'); } - if (this.uuid) { - Overlays.deleteOverlay(this.uuid); - this.uuid = undefined; + if (this.overlayID) { + Overlays.deleteOverlay(this.overlayID); + this.overlayID = undefined; } - this.activeChanged(this.active = false); - // debug plumbing - if (this.$stop) { - this.$stop(); + if (this.active) { + this.activeChanged(this.active = false, reason); + } else if (reason) { + log('already stopped so not triggering another activeChanged; latest reason was:', reason); } }, // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar. - // @param {String} [byJointName=Hips] - the reference joint that will be used to vertically match positions with Avatar - // @note This method attempts to make a direct measurement and then calculate where the doppleganger needs to be - // in order to line-up vertically with the Avatar. Otherwise, animations such as "away" mode can - // result in the doppleganger floating above or below ground level. - syncVerticalPosition: function s(byJointName) { + // @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar + syncVerticalPosition: function(byJointName) { byJointName = byJointName || 'Hips'; - var names = Overlays.getProperty(this.uuid, 'jointNames'), - positions = Overlays.getProperty(this.uuid, 'jointPositions'), - dopplePosition = Overlays.getProperty(this.uuid, 'position'), + var names = Overlays.getProperty(this.overlayID, 'jointNames'), + positions = Overlays.getProperty(this.overlayID, 'jointPositions'), + dopplePosition = Overlays.getProperty(this.overlayID, 'position'), doppleJointIndex = names.indexOf(byJointName), doppleJointPosition = positions[doppleJointIndex]; @@ -267,23 +265,11 @@ Doppleganger.prototype = { log('adjusting for offset', offset); dopplePosition.y = avatarPosition.y + offset; this.position = dopplePosition; - Overlays.editOverlay(this.uuid, { position: this.position }); - }, - - // @private @method - signal handler for Overlays.overlayDeleted - _onDeletedOverlay: function(uuid) { - if (uuid === this.uuid) { - log('onDeletedOverlay', uuid); - this.stop(); - } + Overlays.editOverlay(this.overlayID, { position: this.position }); }, // @private @method - creates the update thread to synchronize joint data _createUpdateThread: function() { - if (!this.autoUpdate) { - log('options.autoUpdate == false -- call .update() manually to sync joint data'); - return; - } if (Doppleganger.USE_SCRIPT_UPDATE) { log('creating Script.update thread'); this.onUpdate = this.update; @@ -296,30 +282,36 @@ Doppleganger.prototype = { }, // @private @method - waits for model to load and handles timeouts - // @note This is needed because sometimes it takes a few frames for the underlying model - // to become loaded even when already cached locally. - _waitForModel: function(callback) { - var RECHECK_MS = 50, MAX_WAIT_MS = 10000; - var id = this.uuid, + // @param {ModelResource} resource - a prefetched resource to monitor loading state against + _waitForModel: function(resource) { + var RECHECK_MS = 50; + var id = this.overlayID, watchdogTimer = null; function waitForJointNames() { + var error = null, result = null; if (!watchdogTimer) { - log('timeout waiting for ModelOverlay jointNames'); - Script.clearInterval(this._interval); - this._interval = null; - return this.modelOverlayLoaded(new Error('could not retrieve jointNames'), null); + error = 'joints_unavailable'; + } else if (resource.state === Resource.State.FAILED) { + error = 'prefetch_failed'; + } else if (resource.state === Resource.State.FINISHED) { + var names = Overlays.getProperty(id, 'jointNames'); + if (Array.isArray(names) && names.length) { + result = { overlayID: id, jointNames: names }; + } } - var names = Overlays.getProperty(id, 'jointNames'); - if (Array.isArray(names) && names.length) { + if (error || result !== null) { Script.clearInterval(this._interval); this._interval = null; - return this.modelOverlayLoaded(null, { uuid: id, jointNames: names }); + if (watchdogTimer) { + Script.clearTimeout(watchdogTimer); + } + this.modelOverlayLoaded(error, result); } } watchdogTimer = Script.setTimeout(function() { watchdogTimer = null; - }, MAX_WAIT_MS); + }, Doppleganger.MAX_WAIT_SECS * 1000); this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS); } }; @@ -373,106 +365,130 @@ function log() { // var doppleganger = new Doppleganger(); // Doppleganger.addDebugControls(doppleganger); Doppleganger.addDebugControls = function(doppleganger) { - var onMousePressEvent, - debugOverlayIDs, - selectedJointName; + DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; + DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; - if ('$update' in doppleganger) { + function DebugControls() { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugOverlayIDs = undefined; + this.jointSelected = signal(function(result) {}); + } + DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(Overlays.deleteOverlay); + this.debugOverlayIDs = undefined; + } + }, + + onJointsUpdated: function(overlayID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Overlays.getProperty(overlayID, 'jointNames'), + jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), + jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), + selectedIndex = jointNames.indexOf(this.selectedJointName); + + if (!this.debugOverlayIDs) { + this.debugOverlayIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i], + rotation: jointOrientations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (!evt.isLeftButton || !this.enableIndicators || !this.debugOverlayIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } + }; + + if ('$debugControls' in doppleganger) { throw new Error('only one set of debug controls can be added per doppleganger'); } + var debugControls = new DebugControls(); + doppleganger.$debugControls = debugControls; - function $start() { - onMousePressEvent = _onMousePressEvent; - Controller.mousePressEvent.connect(this, _onMousePressEvent); - } - - function createOverlays(jointNames) { - return jointNames.map(function(name, i) { - return Overlays.addOverlay('shape', { - shape: 'Icosahedron', - scale: 0.1, - solid: false, - alpha: 0.5 - }); - }); - } - - function cleanupOverlays() { - if (debugOverlayIDs) { - debugOverlayIDs.forEach(Overlays.deleteOverlay); - debugOverlayIDs = undefined; - } - } - - function $stop() { - if (onMousePressEvent) { - Controller.mousePressEvent.disconnect(this, onMousePressEvent); - onMousePressEvent = undefined; - } - cleanupOverlays(); - } - - function $update() { - var COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; - var COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; - - var id = this.uuid, - jointOrientations = Overlays.getProperty(id, 'jointOrientations'), - jointPositions = Overlays.getProperty(id, 'jointPositions'), - selectedIndex = this.jointNames.indexOf(selectedJointName); - - if (!debugOverlayIDs) { - debugOverlayIDs = createOverlays(this.jointNames); - } - - // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) - var updatedOverlays = debugOverlayIDs.reduce(function(updates, id, i) { - updates[id] = { - position: jointPositions[i], - rotation: jointOrientations[i], - color: i === selectedIndex ? COLOR_SELECTED : COLOR_DEFAULT, - solid: i === selectedIndex - }; - return updates; - }, {}); - Overlays.editOverlays(updatedOverlays); - } - - function _onMousePressEvent(evt) { + function onMousePressEvent(evt) { if (evt.isRightButton) { if (evt.isShifted) { - if (this.$update) { - // toggle debug overlays off - cleanupOverlays(); - delete this.$update; - } else { - this.$update = $update; + debugControls.enableIndicators = !debugControls.enableIndicators; + if (!debugControls.enableIndicators) { + debugControls.removeIndicators(); } } else { - this.mirrored = !this.mirrored; + doppleganger.mirrored = !doppleganger.mirrored; } - return; } - if (!evt.isLeftButton || !debugOverlayIDs) { - return; + } + + doppleganger.activeChanged.connect(function(active) { + if (active) { + debugControls.start(); + doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated'); + Controller.mousePressEvent.connect(onMousePressEvent); + } else { + Controller.mousePressEvent.disconnect(onMousePressEvent); + doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated'); + debugControls.stop(); } + }); - var ray = Camera.computePickRay(evt.x, evt.y), - hit = Overlays.findRayIntersection(ray, true, debugOverlayIDs); - - hit.jointIndex = debugOverlayIDs.indexOf(hit.overlayID); + debugControls.jointSelected.connect(function(hit) { + debugControls.selectedJointName = hit.jointName; if (hit.jointIndex < 0) { return; } - hit.jointName = this.jointNames[hit.jointIndex]; - hit.mirroredJointName = this.mirroredNames[hit.jointIndex]; - selectedJointName = hit.jointName; + hit.mirroredJointName = Doppleganger.getMirroredJointNames([hit.jointName])[0]; log('selected joint:', JSON.stringify(hit, 0, 2)); - } + }); - doppleganger.$start = $start; - doppleganger.$stop = $stop; - doppleganger.$update = $update; + Script.scriptEnding.connect(debugControls, 'removeIndicators'); return doppleganger; }; From 6e0793335642f94274c0896f4d1eea21ec6617ca Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 26 Apr 2017 18:28:54 -0400 Subject: [PATCH 9/9] uncomment UserActivityLogger.logAction --- scripts/system/app-doppleganger.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/scripts/system/app-doppleganger.js b/scripts/system/app-doppleganger.js index 9fdcc6fa03..d7f85e5767 100644 --- a/scripts/system/app-doppleganger.js +++ b/scripts/system/app-doppleganger.js @@ -69,14 +69,7 @@ if (Settings.getValue('debug.doppleganger', false)) { DopplegangerClass.addDebugControls(doppleganger); } -// TODO: uncomment after PR #10130 is merged (which provides the needed .logAction method) -/* UserActivityLogger.logAction('doppleganger_app_load'); - -// Script.scriptEnding.connect(function() { -// UserActivityLogger.logAction('doppleganger_app_unload'); -// }); - doppleganger.activeChanged.connect(function(active, reason) { if (active) { UserActivityLogger.logAction('doppleganger_enable'); @@ -90,5 +83,3 @@ doppleganger.activeChanged.connect(function(active, reason) { } } }); - -*/