mirror of
https://github.com/overte-org/overte.git
synced 2025-08-06 18:50:00 +02:00
cleanup and documentation
This commit is contained in:
parent
760113f9c9
commit
a833912399
2 changed files with 270 additions and 160 deletions
|
@ -5,8 +5,8 @@
|
||||||
// Created by Timothy Dedischew on 04/21/2017.
|
// Created by Timothy Dedischew on 04/21/2017.
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
// Copyright 2017 High Fidelity, Inc.
|
||||||
//
|
//
|
||||||
// This tablet app creates a mirrored projection of your avatar (ie: a "doppleganger") that you can walk around
|
// This Client script creates can instance of a Doppleganger that can be toggled on/off via tablet button.
|
||||||
// and inspect.
|
// (for more info see doppleganger.js)
|
||||||
//
|
//
|
||||||
// Distributed under the Apache License, Version 2.0.
|
// Distributed under the Apache License, Version 2.0.
|
||||||
// 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
|
||||||
|
@ -14,39 +14,35 @@
|
||||||
|
|
||||||
/* global */
|
/* global */
|
||||||
|
|
||||||
var TABLET_APP_ICON = Script.resolvePath('Spiegel-lineart-black.svg');
|
var DopplegangerClass = Script.require('./doppleganger.js');
|
||||||
var TABLET_APP_NAME = 'mirror';
|
// 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 tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
||||||
var DEBUG = true;
|
button = tablet.addButton({
|
||||||
var MIRRORED = true; // whether to mirror joints or simply transfer them as-is
|
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');
|
Script.scriptEnding.connect(function() {
|
||||||
var button = tablet.addButton({
|
tablet.removeButton(button);
|
||||||
icon: TABLET_APP_ICON,
|
|
||||||
text: TABLET_APP_NAME
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var DopplegangerClass = Script.require('./doppleganger.js#'+ new Date().getTime().toString(36));
|
|
||||||
|
|
||||||
var doppleganger = new DopplegangerClass({
|
var doppleganger = new DopplegangerClass({
|
||||||
avatar: MyAvatar,
|
avatar: MyAvatar,
|
||||||
mirrored: MIRRORED,
|
mirrored: true,
|
||||||
debug: DEBUG,
|
eyeToEye: true,
|
||||||
eyeToEye: EYE_TO_EYE,
|
autoUpdate: true
|
||||||
});
|
});
|
||||||
|
Script.scriptEnding.connect(doppleganger, 'cleanup');
|
||||||
|
|
||||||
|
if (Settings.getValue('debug.doppleganger', false)) {
|
||||||
|
DopplegangerClass.addDebugControls(doppleganger);
|
||||||
|
}
|
||||||
|
|
||||||
button.clicked.connect(function() {
|
button.clicked.connect(function() {
|
||||||
print('click', doppleganger.active);
|
|
||||||
doppleganger.toggle();
|
doppleganger.toggle();
|
||||||
button.editProperties({ isActive: doppleganger.active });
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -5,63 +5,79 @@
|
||||||
// Created by Timothy Dedischew on 04/21/2017.
|
// Created by Timothy Dedischew on 04/21/2017.
|
||||||
// Copyright 2017 High Fidelity, Inc.
|
// 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.
|
// Distributed under the Apache License, Version 2.0.
|
||||||
// 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 */
|
/* global module */
|
||||||
|
// @module doppleganger
|
||||||
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
|
// 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;
|
module.exports = Doppleganger;
|
||||||
|
|
||||||
if (!Function.prototype.bind) {
|
// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data
|
||||||
// FIXME: this inline version is meant to be temporary, pending either a system-wide version being adopted
|
Doppleganger.USE_SCRIPT_UPDATE = false;
|
||||||
// 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 {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) {
|
function Doppleganger(options) {
|
||||||
|
options = options || {};
|
||||||
this.avatar = options.avatar || MyAvatar;
|
this.avatar = options.avatar || MyAvatar;
|
||||||
this.mirrored = 'mirrored' in options ? options.mirrored : true;
|
this.mirrored = 'mirrored' in options ? options.mirrored : true;
|
||||||
this.debug = 'debug' in options ? options.debug : true;
|
this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true;
|
||||||
this.eyeToEye = 'eyeToEye' in options ? options.eyeToEye : true;
|
|
||||||
|
|
||||||
// instance properties
|
// @public
|
||||||
this.active = false;
|
this.active = false; // whether doppleganger is currently being displayed/updated
|
||||||
this.uuid = null;
|
this.uuid = null; // current doppleganger's Overlay id
|
||||||
this.interval = null;
|
this.ready = false; // whether the underlying ModelOverlay has finished loading
|
||||||
this.selectedJoint = null;
|
this.frame = 0; // current joint update frame
|
||||||
this.positionNeedsUpdate = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Doppleganger.prototype = {
|
Doppleganger.prototype = {
|
||||||
|
// @public @method - toggles doppleganger on/off
|
||||||
toggle: function() {
|
toggle: function() {
|
||||||
if (this.active) {
|
if (this.active) {
|
||||||
print('toggling off');
|
log('toggling off');
|
||||||
this.off();
|
this.off();
|
||||||
this.active = false;
|
this.active = false;
|
||||||
} else {
|
} else {
|
||||||
print('toggling on');
|
log('toggling on');
|
||||||
this.on();
|
this.on();
|
||||||
this.active = true;
|
this.active = true;
|
||||||
}
|
}
|
||||||
return this.active;
|
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() {
|
update: function() {
|
||||||
|
this.frame++;
|
||||||
try {
|
try {
|
||||||
if (!this.uuid) {
|
if (!this.uuid) {
|
||||||
throw new Error('!this.uuid');
|
throw new Error('!this.uuid');
|
||||||
}
|
}
|
||||||
|
|
||||||
var rotations = this.avatar.getJointRotations();
|
var rotations = this.avatar.getJointRotations();
|
||||||
var translations = this.avatar.getJointTranslations() || rotations.map(function(_, i) {
|
var translations = this.avatar.getJointTranslations();
|
||||||
return this.avatar.getJointTranslation(i);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
if (this.mirrored) {
|
if (this.mirrored) {
|
||||||
var mirroredIndexes = this.mirroredIndexes;
|
var mirroredIndexes = this.mirroredIndexes;
|
||||||
|
@ -82,28 +98,29 @@ Doppleganger.prototype = {
|
||||||
translations = outTranslations;
|
translations = outTranslations;
|
||||||
}
|
}
|
||||||
Overlays.editOverlay(this.uuid, {
|
Overlays.editOverlay(this.uuid, {
|
||||||
visible: true,
|
|
||||||
jointRotations: rotations,
|
jointRotations: rotations,
|
||||||
jointTranslations: translations,
|
jointTranslations: translations
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.positionNeedsUpdate) {
|
// debug plumbing
|
||||||
this.positionNeedsUpdate = this.eyeToEye; // only continue updating if seeing eye-to-eye
|
if (this.$update) {
|
||||||
this.fixVerticalPosition();
|
this.$update();
|
||||||
}
|
}
|
||||||
if (this.debug) {
|
} catch (e) {
|
||||||
this._drawDebugOverlays();
|
log('update ERROR: '+ e);
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
print('update ERROR: '+ e);
|
|
||||||
this.off();
|
this.off();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified).
|
||||||
on: function() {
|
on: function() {
|
||||||
if (this.uuid) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
this.ready = false;
|
||||||
|
this.frame = 0;
|
||||||
|
|
||||||
this.position = Vec3.sum(this.avatar.position, Quat.getFront(this.avatar.orientation));
|
this.position = Vec3.sum(this.avatar.position, Quat.getFront(this.avatar.orientation));
|
||||||
this.orientation = this.avatar.orientation;
|
this.orientation = this.avatar.orientation;
|
||||||
this.skeletonModelURL = this.avatar.skeletonModelURL;
|
this.skeletonModelURL = this.avatar.skeletonModelURL;
|
||||||
|
@ -118,62 +135,43 @@ Doppleganger.prototype = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var avatar = this.avatar;
|
||||||
this.mirroredIndexes = this.mirroredNames.map(function(name) {
|
this.mirroredIndexes = this.mirroredNames.map(function(name) {
|
||||||
return name ? this.avatar.getJointIndex(name) : false;
|
return name ? avatar.getJointIndex(name) : false;
|
||||||
}.bind(this));
|
});
|
||||||
|
|
||||||
this.uuid = Overlays.addOverlay('model', {
|
this.uuid = 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._onModelReady(function() {
|
this._onModelOverlayReady(bind(this, function() {
|
||||||
if (USE_SCRIPT_UPDATE) {
|
this.ready = true;
|
||||||
this.onUpdate = this.update;
|
Overlays.editOverlay(this.uuid, { visible: true });
|
||||||
Script.update.connect(this, 'onUpdate');
|
this.syncVerticalPosition();
|
||||||
print('doppleganger will be updated from Script.update');
|
log('ModelOverlay is ready; # joints == ' + Overlays.getProperty(this.uuid, 'jointNames').length);
|
||||||
} else {
|
if (this.autoUpdate) {
|
||||||
print('doppleganger will be updated using Script.setInterval at', TARGET_FPS +'fps');
|
this._createUpdateThread();
|
||||||
this.interval = Script.setInterval(this.update.bind(this), 1000 / TARGET_FPS);
|
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
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;
|
this.onDeletedOverlay = this._onDeletedOverlay;
|
||||||
Overlays.overlayDeleted.connect(this, 'onDeletedOverlay');
|
Overlays.overlayDeleted.connect(this, 'onDeletedOverlay');
|
||||||
|
|
||||||
// FIXME: remove this hook after verifying joint modes between mirrored/non-mirrored
|
// debug plumbing
|
||||||
if (true) {
|
if (this.$on) {
|
||||||
this.onMousePressEvent = this._onMousePressEvent;
|
this.$on();
|
||||||
Controller.mousePressEvent.connect(this, 'onMousePressEvent');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// execute callback only after the ModelOverlay has finished loading
|
// @public @method - hide the doppleganger
|
||||||
_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() {
|
off: function() {
|
||||||
|
this.ready = false;
|
||||||
if (this.onUpdate) {
|
if (this.onUpdate) {
|
||||||
Script.update.disconnect(this, 'onUpdate');
|
Script.update.disconnect(this, 'onUpdate');
|
||||||
delete this.onUpdate;
|
delete this.onUpdate;
|
||||||
|
@ -182,30 +180,27 @@ Doppleganger.prototype = {
|
||||||
Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay');
|
Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay');
|
||||||
delete this.onDeletedOverlay;
|
delete this.onDeletedOverlay;
|
||||||
}
|
}
|
||||||
if (this.onMousePressEvent) {
|
if (this._interval) {
|
||||||
Controller.mousePressEvent.disconnect(this, 'onMousePressEvent');
|
Script.clearInterval(this._interval);
|
||||||
delete this.onMousePressEvent;
|
this._interval = undefined;
|
||||||
}
|
|
||||||
if (this.interval) {
|
|
||||||
Script.clearInterval(this.interval);
|
|
||||||
this.interval = undefined;
|
|
||||||
}
|
}
|
||||||
if (this.uuid) {
|
if (this.uuid) {
|
||||||
Overlays.deleteOverlay(this.uuid);
|
Overlays.deleteOverlay(this.uuid);
|
||||||
this.uuid = undefined;
|
this.uuid = undefined;
|
||||||
}
|
}
|
||||||
if (this.debugOverlayIDs) {
|
// debug plumbing
|
||||||
this.debugOverlayIDs.forEach(function(o) { Overlays.deleteOverlay(o); });
|
if (this.$off) {
|
||||||
this.debugOverlayIDs = undefined;
|
this.$off();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// ModelOverlays & ModelEntities get positioned slightly differently than rigged Avatars
|
// @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar.
|
||||||
// in EYE_TO_EYE mode this helper takes an actual measurement and adjusts the doppleganger
|
// @param {String} [byJointName=Hips] - the reference joint that will be used to vertically match positions with Avatar
|
||||||
// so it always sees "eye to eye" with the avatar
|
// @note This method attempts to make a direct measurement and then calculate where the doppleganger needs to be
|
||||||
fixVerticalPosition: function() {
|
// in order to line-up vertically with the Avatar. Otherwise, animations such as "away" mode can
|
||||||
var byJointName = this.syncJointName || 'Hips';
|
// result in the doppleganger floating above or below ground level.
|
||||||
|
syncVerticalPosition: function s(byJointName) {
|
||||||
|
byJointName = byJointName || 'Hips';
|
||||||
var names = Overlays.getProperty(this.uuid, 'jointNames'),
|
var names = Overlays.getProperty(this.uuid, 'jointNames'),
|
||||||
positions = Overlays.getProperty(this.uuid, 'jointPositions'),
|
positions = Overlays.getProperty(this.uuid, 'jointPositions'),
|
||||||
dopplePosition = Overlays.getProperty(this.uuid, 'position'),
|
dopplePosition = Overlays.getProperty(this.uuid, 'position'),
|
||||||
|
@ -214,77 +209,196 @@ Doppleganger.prototype = {
|
||||||
|
|
||||||
var avatarPosition = this.avatar.position,
|
var avatarPosition = this.avatar.position,
|
||||||
avatarJointIndex = this.avatar.getJointIndex(byJointName),
|
avatarJointIndex = this.avatar.getJointIndex(byJointName),
|
||||||
avatarJointPosition = this.avatar.getAbsoluteJointTranslationInObjectFrame(avatarJointIndex);
|
avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex);
|
||||||
|
|
||||||
var offset = Vec3.subtract(avatarJointPosition, doppleJointPosition);
|
var offset = avatarJointPosition.y - doppleJointPosition.y;
|
||||||
|
log('adjusting for offset', offset);
|
||||||
dopplePosition.y = avatarPosition.y + offset.y;
|
dopplePosition.y = avatarPosition.y + offset;
|
||||||
this.position = dopplePosition;
|
this.position = dopplePosition;
|
||||||
Overlays.editOverlay(this.uuid, { position: this.position });
|
Overlays.editOverlay(this.uuid, { position: this.position });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// @private @method - signal handler for Overlays.overlayDeleted
|
||||||
_onDeletedOverlay: function(uuid) {
|
_onDeletedOverlay: function(uuid) {
|
||||||
print('onDeletedOverlay', uuid);
|
log('onDeletedOverlay', uuid);
|
||||||
if (uuid === this.uuid) {
|
if (uuid === this.uuid) {
|
||||||
this.off();
|
this.off();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// DEBUG methods for verifying mirrored joint behaviors
|
// @private @method - creates the update thread to synchronize joint data
|
||||||
_onMousePressEvent: function(evt) {
|
_createUpdateThread: function() {
|
||||||
if (evt.isRightButton) {
|
if (!this.autoUpdate) {
|
||||||
this.mirrored = !this.mirrored;
|
log('options.autoUpdate == false -- call .update() manually to sync joint data');
|
||||||
}
|
|
||||||
if (!this.debug || !evt.isLeftButton) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var ray = Camera.computePickRay(evt.x, evt.y),
|
if (Doppleganger.USE_SCRIPT_UPDATE) {
|
||||||
hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs);
|
log('creating Script.update thread');
|
||||||
|
this.onUpdate = this.update;
|
||||||
hit.index = this.debugOverlayIDs.indexOf(hit.overlayID);
|
Script.update.connect(this, 'onUpdate');
|
||||||
hit.jointName = this.jointNames[hit.index];
|
} else {
|
||||||
hit.mirroredJointName = this.mirroredNames[hit.index];
|
log('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps');
|
||||||
this.selectedJoint = hit.jointName;
|
var timeout = 1000 / Doppleganger.TARGET_FPS;
|
||||||
print('selected joint:', JSON.stringify(hit,0,2));
|
this._interval = Script.setInterval(bind(this, 'update'), timeout);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_drawDebugOverlays: function() {
|
// @private @method - Invokes a callback once the ModelOverlay is fully-initialized.
|
||||||
const COLOR_DEFAULT = { red: 255, blue: 255, green: 255 };
|
// @param {Function} callback
|
||||||
const COLOR_SELECTED = { red: 0, blue: 255, green: 0 };
|
// @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,
|
var id = this.uuid,
|
||||||
jointOrientations = Overlays.getProperty(id, 'jointOrientations'),
|
jointOrientations = Overlays.getProperty(id, 'jointOrientations'),
|
||||||
jointPositions = Overlays.getProperty(id, 'jointPositions'),
|
jointPositions = Overlays.getProperty(id, 'jointPositions'),
|
||||||
position = Overlays.getProperty(id, 'position'),
|
selectedIndex = this.jointNames.indexOf(selectedJointName);
|
||||||
orientation = Overlays.getProperty(id, 'orientation'),
|
|
||||||
selectedIndex = this.jointNames.indexOf(this.selectedJoint);
|
|
||||||
|
|
||||||
if (!this.debugOverlayIDs) {
|
if (!debugOverlayIDs) {
|
||||||
// set up reference shapes per joint
|
debugOverlayIDs = createOverlays(this.jointNames);
|
||||||
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
|
// batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API)
|
||||||
var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) {
|
var updatedOverlays = debugOverlayIDs.reduce(function(updates, id, i) {
|
||||||
updates[id] = {
|
updates[id] = {
|
||||||
position: jointPositions[i],//Vec3.sum(position, Vec3.multiplyQbyV(orientation, jointPositions[i])),
|
position: jointPositions[i],
|
||||||
rotation: jointOrientations[i],
|
rotation: jointOrientations[i],
|
||||||
color: i === selectedIndex ? COLOR_SELECTED : COLOR_DEFAULT,
|
color: i === selectedIndex ? COLOR_SELECTED : COLOR_DEFAULT,
|
||||||
solid: i === selectedIndex,
|
solid: i === selectedIndex
|
||||||
};
|
};
|
||||||
return updates;
|
return updates;
|
||||||
}, {});
|
}, {});
|
||||||
Overlays.editOverlays(updatedOverlays);
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue