mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-09 08:36:26 +02:00
* 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
This commit is contained in:
parent
060d5aa3cb
commit
cabeced66e
2 changed files with 265 additions and 211 deletions
|
@ -1,5 +1,3 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// doppleganger-app.js
|
// doppleganger-app.js
|
||||||
//
|
//
|
||||||
// Created by Timothy Dedischew on 04/21/2017.
|
// 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
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
|
|
||||||
/* global */
|
|
||||||
|
|
||||||
var DopplegangerClass = Script.require('./doppleganger.js');
|
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'),
|
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
||||||
button = tablet.addButton({
|
button = tablet.addButton({
|
||||||
|
@ -35,22 +29,66 @@ var doppleganger = new DopplegangerClass({
|
||||||
mirrored: true,
|
mirrored: true,
|
||||||
autoUpdate: true
|
autoUpdate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// hide the doppleganger if this client script is unloaded
|
||||||
Script.scriptEnding.connect(doppleganger, 'stop');
|
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) {
|
if (button) {
|
||||||
button.editProperties({ isActive: active });
|
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) {
|
doppleganger.modelOverlayLoaded.connect(function(error, result) {
|
||||||
if (doppleganger.active && error) {
|
if (doppleganger.active && error) {
|
||||||
Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL);
|
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)) {
|
if (Settings.getValue('debug.doppleganger', false)) {
|
||||||
DopplegangerClass.addDebugControls(doppleganger);
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
|
@ -30,6 +30,24 @@ Doppleganger.USE_SCRIPT_UPDATE = false;
|
||||||
// @property {int} - the frame rate to target when using setInterval for joint updates
|
// @property {int} - the frame rate to target when using setInterval for joint updates
|
||||||
Doppleganger.TARGET_FPS = 60;
|
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.
|
// @class Doppleganger - Creates a new instance of a Doppleganger.
|
||||||
// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data.
|
// @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.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints.
|
||||||
|
@ -41,14 +59,16 @@ function Doppleganger(options) {
|
||||||
this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true;
|
this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
this.active = false; // whether doppleganger is currently being displayed/updated
|
this.active = false; // whether doppleganger is currently being displayed/updated
|
||||||
this.uuid = null; // current doppleganger's Overlay id
|
this.overlayID = null; // current doppleganger's Overlay id
|
||||||
this.frame = 0; // current joint update frame
|
this.frame = 0; // current joint update frame
|
||||||
|
|
||||||
// @signal - emitted when .active state changes
|
// @signal - emitted when .active state changes
|
||||||
this.activeChanged = signal(function(active) {});
|
this.activeChanged = signal(function(active, reason) {});
|
||||||
// @signal - emitted once model overlay is either loaded or times out
|
// @signal - emitted once model overlay is either loaded or errors out
|
||||||
this.modelOverlayLoaded = signal(function(error, result){});
|
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 = {
|
Doppleganger.prototype = {
|
||||||
|
@ -64,30 +84,16 @@ Doppleganger.prototype = {
|
||||||
return this.active;
|
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
|
// @public @method - synchronize the joint data between Avatar / doppleganger
|
||||||
update: function() {
|
update: function() {
|
||||||
this.frame++;
|
this.frame++;
|
||||||
try {
|
try {
|
||||||
if (!this.uuid) {
|
if (!this.overlayID) {
|
||||||
throw new Error('!this.uuid');
|
throw new Error('!this.overlayID');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.avatar.skeletonModelURL !== this.skeletonModelURL) {
|
if (this.avatar.skeletonModelURL !== this.skeletonModelURL) {
|
||||||
return this.refreshAvatarModel();
|
return this.stop('avatar_changed');
|
||||||
}
|
}
|
||||||
|
|
||||||
var rotations = this.avatar.getJointRotations();
|
var rotations = this.avatar.getJointRotations();
|
||||||
|
@ -96,18 +102,12 @@ Doppleganger.prototype = {
|
||||||
|
|
||||||
// note: this mismatch can happen when the avatar's model is actively changing
|
// note: this mismatch can happen when the avatar's model is actively changing
|
||||||
if (size !== translations.length ||
|
if (size !== translations.length ||
|
||||||
(this._lastRotationLength && size !== this._lastRotationLength)) {
|
(this.jointStateCount && size !== this.jointStateCount)) {
|
||||||
log('lengths differ', size, translations.length, this._lastRotationLength);
|
log('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount);
|
||||||
this._lastRotationLength = 0;
|
this.stop('avatar_changed_joints');
|
||||||
this.stop();
|
|
||||||
this.skeletonModelURL = null;
|
|
||||||
// wait a second before restarting
|
|
||||||
Script.setTimeout(bind(this, function() {
|
|
||||||
this.refreshAvatarModel(true);
|
|
||||||
}), 1000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._lastRotationLength = size;
|
this.jointStateCount = size;
|
||||||
|
|
||||||
if (this.mirrored) {
|
if (this.mirrored) {
|
||||||
var mirroredIndexes = this.mirroredIndexes;
|
var mirroredIndexes = this.mirroredIndexes;
|
||||||
|
@ -129,93 +129,94 @@ Doppleganger.prototype = {
|
||||||
rotations = outRotations;
|
rotations = outRotations;
|
||||||
translations = outTranslations;
|
translations = outTranslations;
|
||||||
}
|
}
|
||||||
Overlays.editOverlay(this.uuid, {
|
Overlays.editOverlay(this.overlayID, {
|
||||||
jointRotations: rotations,
|
jointRotations: rotations,
|
||||||
jointTranslations: translations
|
jointTranslations: translations
|
||||||
});
|
});
|
||||||
|
|
||||||
// debug plumbing
|
this.jointsUpdated(this.overlayID);
|
||||||
if (this.$update) {
|
|
||||||
this.$update();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('.update error: '+ e, index);
|
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).
|
// @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified).
|
||||||
start: function(transform) {
|
// @param {vec3} [options.position=(in front of avatar)] - starting position
|
||||||
if (this.uuid) {
|
// @param {quat} [options.orientation=avatar.orientation] - starting orientation
|
||||||
log('on() called but overlay model already exists', this.uuid);
|
start: function(options) {
|
||||||
|
options = options || {};
|
||||||
|
if (this.overlayID) {
|
||||||
|
log('start() called but overlay model already exists', this.overlayID);
|
||||||
return;
|
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;
|
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) {
|
this.mirroredIndexes = this.mirroredNames.map(function(name) {
|
||||||
return name ? avatar.getJointIndex(name) : false;
|
return name ? avatar.getJointIndex(name) : false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.uuid = Overlays.addOverlay('model', {
|
this.overlayID = Overlays.addOverlay('model', {
|
||||||
visible: false,
|
visible: false,
|
||||||
url: this.skeletonModelURL,
|
url: this.skeletonModelURL,
|
||||||
position: this.position,
|
position: this.position,
|
||||||
rotation: this.orientation
|
rotation: this.orientation
|
||||||
});
|
});
|
||||||
|
|
||||||
this._waitForModel();
|
|
||||||
this.onModelOverlayLoaded = function(error, result) {
|
this.onModelOverlayLoaded = function(error, result) {
|
||||||
if (error || this.uuid !== result.uuid) {
|
if (error) {
|
||||||
return;
|
return this.stop(error);
|
||||||
}
|
|
||||||
Overlays.editOverlay(this.uuid, { visible: true });
|
|
||||||
if (!transform.position) {
|
|
||||||
this.syncVerticalPosition();
|
|
||||||
}
|
}
|
||||||
log('ModelOverlay is ready; # joints == ' + result.jointNames.length);
|
log('ModelOverlay is ready; # joints == ' + result.jointNames.length);
|
||||||
|
Overlays.editOverlay(this.overlayID, { visible: true });
|
||||||
|
if (!options.position) {
|
||||||
|
this.syncVerticalPosition();
|
||||||
|
}
|
||||||
if (this.autoUpdate) {
|
if (this.autoUpdate) {
|
||||||
this._createUpdateThread();
|
this._createUpdateThread();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.modelOverlayLoaded.connect(this, 'onModelOverlayLoaded');
|
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
|
// 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');
|
Overlays.overlayDeleted.connect(this, 'onDeletedOverlay');
|
||||||
|
|
||||||
if ('onLoadComplete' in this.avatar) {
|
if ('onLoadComplete' in avatar) {
|
||||||
// restart the doppleganger if Avatar loads a different model URL
|
// stop the current doppleganger if Avatar loads a different model URL
|
||||||
this.onLoadComplete = this.refreshAvatarModel;
|
this.onLoadComplete = function() {
|
||||||
this.avatar.onLoadComplete.connect(this, 'onLoadComplete');
|
if (avatar.skeletonModelURL !== this.skeletonModelURL) {
|
||||||
|
this.stop('avatar_changed_load');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
avatar.onLoadComplete.connect(this, 'onLoadComplete');
|
||||||
}
|
}
|
||||||
|
|
||||||
// debug plumbing
|
this.activeChanged(this.active = true, 'start');
|
||||||
if (this.$start) {
|
this._waitForModel(ModelCache.prefetch(this.skeletonModelURL));
|
||||||
this.$start();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// @public @method - hide the doppleganger
|
// @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) {
|
if (this.onUpdate) {
|
||||||
Script.update.disconnect(this, 'onUpdate');
|
Script.update.disconnect(this, 'onUpdate');
|
||||||
delete this.onUpdate;
|
delete this.onUpdate;
|
||||||
|
@ -235,27 +236,24 @@ Doppleganger.prototype = {
|
||||||
if (this.onModelOverlayLoaded) {
|
if (this.onModelOverlayLoaded) {
|
||||||
this.modelOverlayLoaded.disconnect(this, 'onModelOverlayLoaded');
|
this.modelOverlayLoaded.disconnect(this, 'onModelOverlayLoaded');
|
||||||
}
|
}
|
||||||
if (this.uuid) {
|
if (this.overlayID) {
|
||||||
Overlays.deleteOverlay(this.uuid);
|
Overlays.deleteOverlay(this.overlayID);
|
||||||
this.uuid = undefined;
|
this.overlayID = undefined;
|
||||||
}
|
}
|
||||||
this.activeChanged(this.active = false);
|
if (this.active) {
|
||||||
// debug plumbing
|
this.activeChanged(this.active = false, reason);
|
||||||
if (this.$stop) {
|
} else if (reason) {
|
||||||
this.$stop();
|
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.
|
// @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
|
// @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar
|
||||||
// @note This method attempts to make a direct measurement and then calculate where the doppleganger needs to be
|
syncVerticalPosition: function(byJointName) {
|
||||||
// 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';
|
byJointName = byJointName || 'Hips';
|
||||||
var names = Overlays.getProperty(this.uuid, 'jointNames'),
|
var names = Overlays.getProperty(this.overlayID, 'jointNames'),
|
||||||
positions = Overlays.getProperty(this.uuid, 'jointPositions'),
|
positions = Overlays.getProperty(this.overlayID, 'jointPositions'),
|
||||||
dopplePosition = Overlays.getProperty(this.uuid, 'position'),
|
dopplePosition = Overlays.getProperty(this.overlayID, 'position'),
|
||||||
doppleJointIndex = names.indexOf(byJointName),
|
doppleJointIndex = names.indexOf(byJointName),
|
||||||
doppleJointPosition = positions[doppleJointIndex];
|
doppleJointPosition = positions[doppleJointIndex];
|
||||||
|
|
||||||
|
@ -267,23 +265,11 @@ Doppleganger.prototype = {
|
||||||
log('adjusting for offset', offset);
|
log('adjusting for offset', offset);
|
||||||
dopplePosition.y = avatarPosition.y + offset;
|
dopplePosition.y = avatarPosition.y + offset;
|
||||||
this.position = dopplePosition;
|
this.position = dopplePosition;
|
||||||
Overlays.editOverlay(this.uuid, { position: this.position });
|
Overlays.editOverlay(this.overlayID, { position: this.position });
|
||||||
},
|
|
||||||
|
|
||||||
// @private @method - signal handler for Overlays.overlayDeleted
|
|
||||||
_onDeletedOverlay: function(uuid) {
|
|
||||||
if (uuid === this.uuid) {
|
|
||||||
log('onDeletedOverlay', uuid);
|
|
||||||
this.stop();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// @private @method - creates the update thread to synchronize joint data
|
// @private @method - creates the update thread to synchronize joint data
|
||||||
_createUpdateThread: function() {
|
_createUpdateThread: function() {
|
||||||
if (!this.autoUpdate) {
|
|
||||||
log('options.autoUpdate == false -- call .update() manually to sync joint data');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Doppleganger.USE_SCRIPT_UPDATE) {
|
if (Doppleganger.USE_SCRIPT_UPDATE) {
|
||||||
log('creating Script.update thread');
|
log('creating Script.update thread');
|
||||||
this.onUpdate = this.update;
|
this.onUpdate = this.update;
|
||||||
|
@ -296,30 +282,36 @@ Doppleganger.prototype = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// @private @method - waits for model to load and handles timeouts
|
// @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
|
// @param {ModelResource} resource - a prefetched resource to monitor loading state against
|
||||||
// to become loaded even when already cached locally.
|
_waitForModel: function(resource) {
|
||||||
_waitForModel: function(callback) {
|
var RECHECK_MS = 50;
|
||||||
var RECHECK_MS = 50, MAX_WAIT_MS = 10000;
|
var id = this.overlayID,
|
||||||
var id = this.uuid,
|
|
||||||
watchdogTimer = null;
|
watchdogTimer = null;
|
||||||
|
|
||||||
function waitForJointNames() {
|
function waitForJointNames() {
|
||||||
|
var error = null, result = null;
|
||||||
if (!watchdogTimer) {
|
if (!watchdogTimer) {
|
||||||
log('timeout waiting for ModelOverlay jointNames');
|
error = 'joints_unavailable';
|
||||||
Script.clearInterval(this._interval);
|
} else if (resource.state === Resource.State.FAILED) {
|
||||||
this._interval = null;
|
error = 'prefetch_failed';
|
||||||
return this.modelOverlayLoaded(new Error('could not retrieve jointNames'), null);
|
} 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 (error || result !== null) {
|
||||||
if (Array.isArray(names) && names.length) {
|
|
||||||
Script.clearInterval(this._interval);
|
Script.clearInterval(this._interval);
|
||||||
this._interval = null;
|
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 = Script.setTimeout(function() {
|
||||||
watchdogTimer = null;
|
watchdogTimer = null;
|
||||||
}, MAX_WAIT_MS);
|
}, Doppleganger.MAX_WAIT_SECS * 1000);
|
||||||
this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS);
|
this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -373,106 +365,130 @@ function log() {
|
||||||
// var doppleganger = new Doppleganger();
|
// var doppleganger = new Doppleganger();
|
||||||
// Doppleganger.addDebugControls(doppleganger);
|
// Doppleganger.addDebugControls(doppleganger);
|
||||||
Doppleganger.addDebugControls = function(doppleganger) {
|
Doppleganger.addDebugControls = function(doppleganger) {
|
||||||
var onMousePressEvent,
|
DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 };
|
||||||
debugOverlayIDs,
|
DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 };
|
||||||
selectedJointName;
|
|
||||||
|
|
||||||
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');
|
throw new Error('only one set of debug controls can be added per doppleganger');
|
||||||
}
|
}
|
||||||
|
var debugControls = new DebugControls();
|
||||||
|
doppleganger.$debugControls = debugControls;
|
||||||
|
|
||||||
function $start() {
|
function onMousePressEvent(evt) {
|
||||||
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) {
|
|
||||||
if (evt.isRightButton) {
|
if (evt.isRightButton) {
|
||||||
if (evt.isShifted) {
|
if (evt.isShifted) {
|
||||||
if (this.$update) {
|
debugControls.enableIndicators = !debugControls.enableIndicators;
|
||||||
// toggle debug overlays off
|
if (!debugControls.enableIndicators) {
|
||||||
cleanupOverlays();
|
debugControls.removeIndicators();
|
||||||
delete this.$update;
|
|
||||||
} else {
|
|
||||||
this.$update = $update;
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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),
|
debugControls.jointSelected.connect(function(hit) {
|
||||||
hit = Overlays.findRayIntersection(ray, true, debugOverlayIDs);
|
debugControls.selectedJointName = hit.jointName;
|
||||||
|
|
||||||
hit.jointIndex = debugOverlayIDs.indexOf(hit.overlayID);
|
|
||||||
if (hit.jointIndex < 0) {
|
if (hit.jointIndex < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hit.jointName = this.jointNames[hit.jointIndex];
|
hit.mirroredJointName = Doppleganger.getMirroredJointNames([hit.jointName])[0];
|
||||||
hit.mirroredJointName = this.mirroredNames[hit.jointIndex];
|
|
||||||
selectedJointName = hit.jointName;
|
|
||||||
log('selected joint:', JSON.stringify(hit, 0, 2));
|
log('selected joint:', JSON.stringify(hit, 0, 2));
|
||||||
}
|
});
|
||||||
|
|
||||||
doppleganger.$start = $start;
|
Script.scriptEnding.connect(debugControls, 'removeIndicators');
|
||||||
doppleganger.$stop = $stop;
|
|
||||||
doppleganger.$update = $update;
|
|
||||||
|
|
||||||
return doppleganger;
|
return doppleganger;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue