mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-04-07 10:02:24 +02:00
detect and update doppleganger when user changes skeletonModelURLs; cleanup
This commit is contained in:
parent
a833912399
commit
84c8b2945a
2 changed files with 142 additions and 60 deletions
|
@ -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 });
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue