mirror of
https://github.com/overte-org/overte.git
synced 2025-04-29 18:02:35 +02:00
332 lines
13 KiB
JavaScript
332 lines
13 KiB
JavaScript
// model-helper.js
|
|
//
|
|
// Created by Timothy Dedischew on 06/01/2017.
|
|
// Copyright 2017 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
/* eslint-env commonjs */
|
|
/* global console */
|
|
// @module model-helper
|
|
//
|
|
// This module provides ModelReadyWatcher (a helper class for knowing when a model becomes usable inworld) and
|
|
// also initial plumbing helpers to eliminate unnecessary API differences when working with Model Overlays and
|
|
// Model Entities at a high-level from scripting.
|
|
|
|
var utils = require('./utils.js'),
|
|
assert = utils.assert;
|
|
|
|
module.exports = {
|
|
version: '0.0.1',
|
|
ModelReadyWatcher: ModelReadyWatcher
|
|
};
|
|
|
|
function log() {
|
|
// eslint-disable-next-line no-console
|
|
(typeof console === 'object' ? console.log : print)('model-helper | ' + [].slice.call(arguments).join(' '));
|
|
}
|
|
log(module.exports.version);
|
|
|
|
var _objectDeleted = utils.signal(function objectDeleted(objectID){});
|
|
// proxy for _objectDeleted that only binds deletion tracking if script actually connects to the unified signal
|
|
var objectDeleted = utils.assign(function objectDeleted(objectID){}, {
|
|
connect: function() {
|
|
Overlays.overlayDeleted.connect(_objectDeleted);
|
|
// Entities.deletingEntity.connect(objectDeleted);
|
|
Script.scriptEnding.connect(function() {
|
|
Overlays.overlayDeleted.disconnect(_objectDeleted);
|
|
// Entities.deletingEntity.disconnect(objectDeleted);
|
|
});
|
|
// hereafter _objectDeleted.connect will be used instead
|
|
this.connect = utils.bind(_objectDeleted, 'connect');
|
|
return this.connect.apply(this, arguments);
|
|
},
|
|
disconnect: utils.bind(_objectDeleted, 'disconnect')
|
|
});
|
|
|
|
var modelHelper = module.exports.modelHelper = {
|
|
// Entity <-> Overlay property translations
|
|
_entityFromOverlay: {
|
|
modelURL: function url() {
|
|
return this.url;
|
|
},
|
|
dimensions: function dimensions() {
|
|
return Vec3.multiply(this.scale, this.naturalDimensions);
|
|
}
|
|
},
|
|
_overlayFromEntity: {
|
|
url: function modelURL() {
|
|
return this.modelURL;
|
|
},
|
|
scale: function scale() {
|
|
return this.dimensions && this.naturalDimensions && {
|
|
x: this.dimensions.x / this.naturalDimensions.x,
|
|
y: this.dimensions.y / this.naturalDimensions.y,
|
|
z: this.dimensions.z / this.naturalDimensions.z
|
|
};
|
|
}
|
|
},
|
|
objectDeleted: objectDeleted,
|
|
type: function(objectID) {
|
|
// TODO: support Model Entities (by detecting type from objectID, which is already possible)
|
|
return !Uuid.isNull(objectID) ? 'overlay' : null;
|
|
},
|
|
addObject: function(properties) {
|
|
var type = 'overlay'; // this.resolveType(properties)
|
|
switch (type) {
|
|
case 'overlay': return Overlays.addOverlay(properties.type, this.toOverlayProps(properties));
|
|
}
|
|
return false;
|
|
},
|
|
editObject: function(objectID, properties) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.editOverlay(objectID, this.toOverlayProps(properties));
|
|
}
|
|
return false;
|
|
},
|
|
deleteObject: function(objectID) {
|
|
return this.type(objectID) === 'overlay' && Overlays.deleteOverlay(objectID);
|
|
},
|
|
getProperty: function(objectID, propertyName) {
|
|
return this.type(objectID) === 'overlay' && Overlays.getProperty(objectID, propertyName);
|
|
},
|
|
getProperties: function(objectID, filter) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay':
|
|
filter = Array.isArray(filter) ? filter : [
|
|
'position', 'rotation', 'localPosition', 'localRotation', 'parentID',
|
|
'parentJointIndex', 'scale', 'name', 'visible', 'type', 'url',
|
|
'dimensions', 'naturalDimensions', 'grabbable'
|
|
];
|
|
var properties = filter.reduce(function(out, propertyName) {
|
|
out[propertyName] = Overlays.getProperty(objectID, propertyName);
|
|
return out;
|
|
}, {});
|
|
return this.toEntityProps(properties);
|
|
}
|
|
return null;
|
|
},
|
|
// adapt Entity conventions to Overlay (eg: { modelURL: ... } -> { url: ... })
|
|
toOverlayProps: function(properties) {
|
|
var result = {};
|
|
for (var from in this._overlayFromEntity) {
|
|
var adapter = this._overlayFromEntity[from];
|
|
result[from] = adapter.call(properties, from, adapter.name);
|
|
}
|
|
return utils.assign(result, properties);
|
|
},
|
|
// adapt Overlay conventions to Entity (eg: { url: ... } -> { modelURL: ... })
|
|
toEntityProps: function(properties) {
|
|
var result = {};
|
|
for (var from in this._entityToOverlay) {
|
|
var adapter = this._entityToOverlay[from];
|
|
result[from] = adapter.call(properties, from, adapter.name);
|
|
}
|
|
return utils.assign(result, properties);
|
|
},
|
|
editObjects: function(updatedObjects) {
|
|
var objectIDs = Object.keys(updatedObjects),
|
|
type = this.type(objectIDs[0]);
|
|
switch (type) {
|
|
case 'overlay':
|
|
var translated = {};
|
|
for (var objectID in updatedObjects) {
|
|
translated[objectID] = this.toOverlayProps(updatedObjects[objectID]);
|
|
}
|
|
return Overlays.editOverlays(translated);
|
|
}
|
|
return false;
|
|
},
|
|
getJointIndex: function(objectID, name) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointNames').indexOf(name);
|
|
}
|
|
return -1;
|
|
},
|
|
getJointNames: function(objectID) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointNames');
|
|
}
|
|
return [];
|
|
},
|
|
// @function - derives 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`)
|
|
deriveMirroredJointNames: 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;
|
|
});
|
|
},
|
|
getJointPosition: function(objectID, index) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointPositions')[index];
|
|
}
|
|
return Vec3.ZERO;
|
|
},
|
|
getJointPositions: function(objectID) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointPositions');
|
|
}
|
|
return [];
|
|
},
|
|
getJointOrientation: function(objectID, index) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations')[index];
|
|
}
|
|
return Quat.normalize({});
|
|
},
|
|
getJointOrientations: function(objectID) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations');
|
|
}
|
|
},
|
|
getJointTranslation: function(objectID, index) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations')[index];
|
|
}
|
|
return Vec3.ZERO;
|
|
},
|
|
getJointTranslations: function(objectID) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations');
|
|
}
|
|
return [];
|
|
},
|
|
getJointRotation: function(objectID, index) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointRotations')[index];
|
|
}
|
|
return Quat.normalize({});
|
|
},
|
|
getJointRotations: function(objectID) {
|
|
switch (this.type(objectID)) {
|
|
case 'overlay': return Overlays.getProperty(objectID, 'jointRotations');
|
|
}
|
|
return [];
|
|
}
|
|
}; // modelHelper
|
|
|
|
|
|
// @property {PreconditionFunction} - indicates when the model's jointNames have become available
|
|
ModelReadyWatcher.JOINTS = function(state) {
|
|
return Array.isArray(state.jointNames);
|
|
};
|
|
// @property {PreconditionFunction} - indicates when a model's naturalDimensions have become available
|
|
ModelReadyWatcher.DIMENSIONS = function(state) {
|
|
return Vec3.length(state.naturalDimensions) > 0;
|
|
};
|
|
// @property {PreconditionFunction} - indicates when both a model's naturalDimensions and jointNames have become available
|
|
ModelReadyWatcher.JOINTS_AND_DIMENSIONS = function(state) {
|
|
// eslint-disable-next-line new-cap
|
|
return ModelReadyWatcher.JOINTS(state) && ModelReadyWatcher.DIMENSIONS(state);
|
|
};
|
|
// @property {int} - interval used for continually rechecking model readiness, until ready or a timeout occurs
|
|
ModelReadyWatcher.RECHECK_MS = 50;
|
|
// @property {int} - default wait time before considering a model unready-able.
|
|
ModelReadyWatcher.DEFAULT_TIMEOUT_SECS = 10;
|
|
|
|
// @private @class - waits for model to become usable inworld and tracks errors/timeouts
|
|
// @param [Object} options -- key/value config options:
|
|
// @param {ModelResource} options.resource - a ModelCache prefetched resource to observe for determining load state
|
|
// @param {Uuid} options.objectID - an inworld object to observe for determining readiness states
|
|
// @param {Function} [options.precondition=ModelReadyWatcher.JOINTS] - the precondition used to determine if the model is usable
|
|
// @param {int} [options.maxWaitSeconds=10] - max seconds to wait for the model to become usable, after which a timeout error is emitted
|
|
// @return {ModelReadyWatcher}
|
|
function ModelReadyWatcher(options) {
|
|
options = utils.assign({
|
|
precondition: ModelReadyWatcher.JOINTS,
|
|
maxWaitSeconds: ModelReadyWatcher.DEFAULT_TIMEOUT_SECS
|
|
}, options);
|
|
|
|
assert(!Uuid.isNull(options.objectID), 'Error: invalid options.objectID');
|
|
assert(options.resource && 'state' in options.resource, 'Error: invalid options.resource');
|
|
assert(typeof options.precondition === 'function', 'Error: invalid options.precondition');
|
|
|
|
utils.assign(this, {
|
|
resource: options.resource,
|
|
objectID: options.objectID,
|
|
precondition: options.precondition,
|
|
|
|
// @signal - emitted when the model becomes ready, or with the error that prevented it
|
|
modelReady: utils.signal(function modelReady(error, result){}),
|
|
|
|
// @public
|
|
ready: false, // tracks readiness state
|
|
jointNames: null, // populated with detected jointNames
|
|
naturalDimensions: null, // populated with detected naturalDimensions
|
|
|
|
_startTime: new Date,
|
|
_watchdogTimer: Script.setTimeout(utils.bind(this, function() {
|
|
this._watchdogTimer = null;
|
|
}), options.maxWaitSeconds * 1000),
|
|
_interval: Script.setInterval(utils.bind(this, '_waitUntilReady'), ModelReadyWatcher.RECHECK_MS)
|
|
});
|
|
|
|
this.modelReady.connect(this, function(error, result) {
|
|
this.ready = !error && result;
|
|
});
|
|
}
|
|
|
|
ModelReadyWatcher.prototype = {
|
|
contructor: ModelReadyWatcher,
|
|
// @public method -- cancels monitoring and (if model was not yet ready) emits an error
|
|
cancel: function() {
|
|
return this._stop() && !this.ready && this.modelReady('cancelled', null);
|
|
},
|
|
// stop pending timers
|
|
_stop: function() {
|
|
var stopped = 0;
|
|
if (this._watchdogTimer) {
|
|
Script.clearTimeout(this._watchdogTimer);
|
|
this._watchdogTimer = null;
|
|
stopped++;
|
|
}
|
|
if (this._interval) {
|
|
Script.clearInterval(this._interval);
|
|
this._interval = null;
|
|
stopped++;
|
|
}
|
|
return stopped;
|
|
},
|
|
// the monitoring thread func
|
|
_waitUntilReady: function() {
|
|
var error = null, result = null;
|
|
if (!this._watchdogTimer) {
|
|
error = this.precondition.name || 'timeout';
|
|
} else if (this.resource.state === Resource.State.FAILED) {
|
|
error = 'prefetch_failed';
|
|
} else if (this.resource.state === Resource.State.FINISHED) {
|
|
// in theory there will always be at least one "joint name" that represents the main submesh
|
|
var names = modelHelper.getJointNames(this.objectID);
|
|
if (Array.isArray(names) && names.length) {
|
|
this.jointNames = names;
|
|
}
|
|
var props = modelHelper.getProperties(this.objectID, ['naturalDimensions']);
|
|
if (props && props.naturalDimensions && Vec3.length(props.naturalDimensions)) {
|
|
this.naturalDimensions = props.naturalDimensions;
|
|
}
|
|
var state = {
|
|
resource: this.resource,
|
|
objectID: this.objectID,
|
|
waitTime: (new Date - this._startTime) / 1000,
|
|
jointNames: this.jointNames,
|
|
naturalDimensions: this.naturalDimensions
|
|
};
|
|
if (this.precondition(state)) {
|
|
result = state;
|
|
}
|
|
}
|
|
if (error || result !== null) {
|
|
this._stop();
|
|
this.modelReady(error, result);
|
|
}
|
|
}
|
|
}; // ModelReadyWatcher.prototype
|