mirror of
https://github.com/overte-org/overte.git
synced 2025-04-26 12:36:22 +02:00
457 lines
18 KiB
JavaScript
457 lines
18 KiB
JavaScript
// _utils.js -- misc. helper classes/functions
|
|
|
|
"use strict";
|
|
/* eslint-env commonjs, hifi */
|
|
/* eslint-disable comma-dangle, no-empty */
|
|
/* global HIRES_CLOCK, Desktop, OverlayWebWindow */
|
|
// var HIRES_CLOCK = (typeof Window === 'object' && Window && Window.performance) && Window.performance.now;
|
|
var USE_HIRES_CLOCK = typeof HIRES_CLOCK === 'function';
|
|
|
|
var exports = {
|
|
version: '0.0.1c' + (USE_HIRES_CLOCK ? '-hires' : ''),
|
|
bind: bind,
|
|
signal: signal,
|
|
assign: assign,
|
|
sortedAssign: sortedAssign,
|
|
sign: sign,
|
|
assert: assert,
|
|
makeDebugRequire: makeDebugRequire,
|
|
DeferredUpdater: DeferredUpdater,
|
|
KeyListener: KeyListener,
|
|
getRuntimeSeconds: getRuntimeSeconds,
|
|
createAnimationStepper: createAnimationStepper,
|
|
reloadClientScript: reloadClientScript,
|
|
|
|
normalizeStackTrace: normalizeStackTrace,
|
|
BrowserUtils: BrowserUtils,
|
|
};
|
|
try {
|
|
module.exports = exports; // Interface / Node.js
|
|
} catch (e) {
|
|
this._utils = assign(this._utils || {}, exports); // browser
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
function makeDebugRequire(relativeTo) {
|
|
return function boundDebugRequire(id) {
|
|
return debugRequire(id, relativeTo);
|
|
};
|
|
}
|
|
function debugRequire(id, relativeTo) {
|
|
if (typeof Script === 'object') {
|
|
relativeTo = (relativeTo||Script.resolvePath('.')).replace(/\/+$/, '');
|
|
// hack-around for use during local development / testing that forces every require to re-fetch the script from the server
|
|
var modulePath = Script._requireResolve(id, relativeTo+'/') + '?' + new Date().getTime().toString(36);
|
|
print('========== DEBUGREQUIRE:' + modulePath);
|
|
Script.require.cache[modulePath] = Script.require.cache[id] = undefined;
|
|
Script.require.__qt_data__[modulePath] = Script.require.__qt_data__[id] = true;
|
|
return Script.require(modulePath);
|
|
} else {
|
|
return require(id);
|
|
}
|
|
}
|
|
|
|
// examples:
|
|
// assert(function assertion() { return (conditions === true) }, 'assertion failed!')
|
|
// var neededValue = assert(idString, 'idString not specified!');
|
|
// assert(false, 'unexpected state');
|
|
function assert(truthy, message) {
|
|
message = message || 'Assertion Failed:';
|
|
|
|
if (typeof truthy === 'function' && truthy.name === 'assertion') {
|
|
// extract function body to display with the assertion message
|
|
var assertion = (truthy+'').replace(/[\r\n]/g, ' ')
|
|
.replace(/^[^{]+\{|\}$|^\s*|\s*$/g, '').trim()
|
|
.replace(/^return /,'').replace(/\s[\r\n\t\s]+/g, ' ');
|
|
message += ' ' + JSON.stringify(assertion);
|
|
try {
|
|
truthy = truthy();
|
|
} catch (e) {
|
|
message += '(exception: ' + e +')';
|
|
}
|
|
}
|
|
if (!truthy) {
|
|
message += ' ('+truthy+')';
|
|
throw new Error(message);
|
|
}
|
|
return truthy;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
function sign(x) {
|
|
x = +x;
|
|
if (x === 0 || isNaN(x)) {
|
|
return Number(x);
|
|
}
|
|
return x > 0 ? 1 : -1;
|
|
}
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
|
/* eslint-disable */
|
|
function assign(target, varArgs) { // .length of function is 2
|
|
'use strict';
|
|
if (target == null) { // TypeError if undefined or null
|
|
throw new TypeError('Cannot convert undefined or null to object');
|
|
}
|
|
|
|
var to = Object(target);
|
|
|
|
for (var index = 1; index < arguments.length; index++) {
|
|
var nextSource = arguments[index];
|
|
|
|
if (nextSource != null) { // Skip over if undefined or null
|
|
for (var nextKey in nextSource) {
|
|
// Avoid bugs when hasOwnProperty is shadowed
|
|
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
|
to[nextKey] = nextSource[nextKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return to;
|
|
}
|
|
/* eslint-enable */
|
|
// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
|
|
|
// hack to sort keys in v8 for prettier JSON exports
|
|
function sortedAssign(target, sources) {
|
|
var allParams = assign.apply(this, [{}].concat([].slice.call(arguments)));
|
|
for (var p in target) {
|
|
delete target[p];
|
|
}
|
|
Object.keys(allParams).sort(function(a,b) {
|
|
function swapCase(ch) {
|
|
return /[A-Z]/.test(ch) ? ch.toLowerCase() : ch.toUpperCase();
|
|
}
|
|
a = a.replace(/^./, swapCase);
|
|
b = b.replace(/^./, swapCase);
|
|
a = /Version/.test(a) ? 'AAAA'+a : a;
|
|
b = /Version/.test(b) ? 'AAAA'+b : b;
|
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
}).forEach(function(key) {
|
|
target[key] = allParams[key];
|
|
});
|
|
return target;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// @function - bind a function to a `this` context
|
|
// @param {Object} - the `this` context
|
|
// @param {Function|String} - function or method name
|
|
bind.debug = true;
|
|
function bind(thiz, method) {
|
|
var methodName = typeof method === 'string' ? method : method.name;
|
|
method = thiz[method] || method;
|
|
if (bind.debug && methodName) {
|
|
methodName = methodName.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
var debug = {};
|
|
debug[methodName] = method;
|
|
return eval('1,function bound'+methodName+'() { return debug.'+methodName+'.apply(thiz, arguments); }');
|
|
}
|
|
return function() {
|
|
return method.apply(thiz, arguments);
|
|
};
|
|
}
|
|
|
|
// @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 normalizeStackTrace(err, options) {
|
|
options = options || {};
|
|
// * Chromium: " at <anonymous> (file://.../filename.js:45:65)"
|
|
// * Interface: " at <anonymous() file://.../filename.js:45"
|
|
// * normalized: " at <anonymous> filename.js:45"
|
|
var output = err.stack ? err.stack.replace(
|
|
/((?:https?|file):[/][/].*?):(\d+)(?::\d+)?([)]|\s|$)/g,
|
|
function(_, url, lineNumber, suffix) {
|
|
var fileref = url.split(/[?#]/)[0].split('/').pop();
|
|
if (Array.isArray(options.wrapFilesWith)) {
|
|
fileref = options.wrapFilesWith[0] + fileref + options.wrapFilesWith[1];
|
|
}
|
|
if (Array.isArray(options.wrapLineNumbersWith)) {
|
|
lineNumber = options.wrapLineNumbersWith[0] + lineNumber + options.wrapLineNumbersWith[1];
|
|
}
|
|
return fileref + ':' + lineNumber + suffix;
|
|
}
|
|
).replace(/[(]([-\w.%:]+[.](?:html|js))[)]/g, '$1') : err.message;
|
|
return ' '+output;
|
|
}
|
|
|
|
// utilities specific to use from web browsers / embedded Interface web windows
|
|
function BrowserUtils(global) {
|
|
global = global || (1,eval)('this');
|
|
return {
|
|
global: global,
|
|
console: global.console,
|
|
log: function(msg) { return this.console.log('browserUtils | ' + [].slice.call(arguments).join(' ')); },
|
|
makeConsoleWorkRight: function(console, forcePatching) {
|
|
if (console.$patched || !(forcePatching || global.qt)) {
|
|
return console;
|
|
}
|
|
var patched = ['log','debug','info','warn','error'].reduce(function(output, method) {
|
|
output[method] = function() {
|
|
return console[method]([].slice.call(arguments).join(' '));
|
|
};
|
|
return output;
|
|
}, { $patched: console });
|
|
for (var p in console) {
|
|
if (typeof console[p] === 'function' && !(p in patched)) {
|
|
patched[p] = console[p].bind(console);
|
|
}
|
|
}
|
|
patched.__proto__ = console; // let scope chain find constants and other non-function values
|
|
return patched;
|
|
},
|
|
parseQueryParams: function(querystring) {
|
|
return this.extendWithQueryParams({}, querystring);
|
|
},
|
|
extendWithQueryParams: function(obj, querystring) {
|
|
querystring = querystring || global.location.href;
|
|
querystring.replace(/\b(\w+)=([^&?#]+)/g, function(_, key, value) {
|
|
value = unescape(value);
|
|
obj[key] = value;
|
|
});
|
|
return obj;
|
|
},
|
|
// openEventBridge handles the cluster of scenarios Interface has imposed on webviews for making EventBridge connections
|
|
openEventBridge: function openEventBridge(callback) {
|
|
this.log('openEventBridge |', 'typeof global.EventBridge == ' + [typeof global.EventBridge, global.EventBridge ]);
|
|
var error;
|
|
try {
|
|
global.EventBridge.toString = function() { return '[global.EventBridge at startup]'; };
|
|
global.EventBridge.scriptEventReceived.connect.exists;
|
|
// this.log('openEventBridge| EventBridge already exists... -- invoking callback', 'typeof EventBridge == ' + typeof global.EventBridge);
|
|
try {
|
|
return callback(global.EventBridge);
|
|
} catch(e) {
|
|
error = e;
|
|
}
|
|
} catch (e) {
|
|
this.log('EventBridge not found in a usable state -- attempting to instrument via qt.webChannelTransport',
|
|
Object.keys(global.EventBridge||{}));
|
|
var QWebChannel = assert(global.QWebChannel, 'expected global.QWebChannel to exist'),
|
|
qt = assert(global.qt, 'expected global.qt to exist');
|
|
assert(qt.webChannelTransport, 'expected global.qt.webChannelTransport to exist');
|
|
new QWebChannel(qt.webChannelTransport, bind(this, function (channel) {
|
|
var objects = channel.objects;
|
|
if (global.EventBridge) {
|
|
log('>>> global.EventBridge was unavailable at page load, but has spontaneously materialized; ' +
|
|
[ typeof global.EventBridge, global.EventBridge ]);
|
|
}
|
|
var eventBridge = objects.eventBridge || (objects.eventBridgeWrapper && objects.eventBridgeWrapper.eventBridge);
|
|
eventBridge.toString = function() { return '[window.EventBridge per QWebChannel]'; };
|
|
assert(!global.EventBridge || global.EventBridge === eventBridge, 'global.EventBridge !== QWebChannel eventBridge\n' +
|
|
[global.EventBridge, eventBridge]);
|
|
global.EventBridge = eventBridge;
|
|
global.EventBridge.$WebChannel = channel;
|
|
this.log('openEventBridge opened -- invoking callback', 'typeof EventBridge === ' + typeof global.EventBridge);
|
|
callback(global.EventBridge);
|
|
}));
|
|
}
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
// ----------------------------------------------------------------------------
|
|
// queue property/method updates to target so that they can be applied all-at-once
|
|
function DeferredUpdater(target, options) {
|
|
options = options || {};
|
|
// define _meta as a non-enumerable (so it doesn't show up in for (var p in ...) loops)
|
|
Object.defineProperty(this, '_meta', { enumerable: false, value: {
|
|
target: target,
|
|
lastValue: {},
|
|
dedupe: options.dedupe,
|
|
}});
|
|
}
|
|
DeferredUpdater.prototype = {
|
|
reset: function() {
|
|
var self = this;
|
|
Object.keys(this).forEach(function(property) {
|
|
delete self[property];
|
|
});
|
|
this._meta.lastValue = {};
|
|
},
|
|
submit: function() {
|
|
var meta = this._meta,
|
|
target = meta.target,
|
|
self = this,
|
|
submitted = {};
|
|
self.submit = getRuntimeSeconds();
|
|
Object.keys(self).forEach(function(property) {
|
|
var newValue = self[property];
|
|
submitted[property] = newValue;
|
|
if (typeof target[property] === 'function') {
|
|
target[property](newValue);
|
|
} else {
|
|
target[property] = newValue;
|
|
}
|
|
delete self[property];
|
|
});
|
|
return submitted;
|
|
}
|
|
};
|
|
// create a group of deferred updaters eg: DeferredUpdater.createGroup({ MyAvatar: MyAvatar, Camera: Camera })
|
|
DeferredUpdater.createGroup = function(items, options) {
|
|
var result = {
|
|
__proto__: {
|
|
reset: function() {
|
|
Object.keys(this).forEach(bind(this, function(item) {
|
|
this[item].reset();
|
|
}));
|
|
},
|
|
submit: function() {
|
|
var submitted = {};
|
|
Object.keys(this).forEach(bind(this, function(item) {
|
|
submitted[item] = this[item].submit();
|
|
}));
|
|
return submitted;
|
|
}
|
|
}
|
|
};
|
|
Object.keys(items).forEach(function(item) {
|
|
result[item] = new DeferredUpdater(items[item], options);
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// session runtime in seconds
|
|
getRuntimeSeconds.EPOCH = getRuntimeSeconds(0);
|
|
function getRuntimeSeconds(since) {
|
|
since = since === undefined ? getRuntimeSeconds.EPOCH : since;
|
|
var now = USE_HIRES_CLOCK ? new HIRES_CLOCK() : +new Date;
|
|
return ((now / 1000.0) - since);
|
|
}
|
|
|
|
// requestAnimationFrame emulation
|
|
function createAnimationStepper(options) {
|
|
options = options || {};
|
|
var fps = options.fps || 30,
|
|
waitMs = 1000 / fps,
|
|
getTime = options.getRuntimeSeconds || getRuntimeSeconds,
|
|
lastUpdateTime = -1e-6,
|
|
timeout = 0;
|
|
|
|
requestAnimationFrame.fps = fps;
|
|
requestAnimationFrame.reset = function() {
|
|
if (timeout) {
|
|
Script.clearTimeout(timeout);
|
|
timeout = 0;
|
|
}
|
|
};
|
|
|
|
function requestAnimationFrame(update) {
|
|
requestAnimationFrame.reset();
|
|
timeout = Script.setTimeout(function() {
|
|
timeout = 0;
|
|
update(getTime(lastUpdateTime));
|
|
lastUpdateTime = getTime();
|
|
}, waitMs );
|
|
}
|
|
|
|
return requestAnimationFrame;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// KeyListener provides a scoped wrapper where options.onKeyPressEvent gets
|
|
// called when a key event matches the specified event.text / key spec
|
|
// example: var listener = new KeyListener({ text: 'SPACE', isShifted: false, onKeyPressEvent: function(event) { ... } });
|
|
// Script.scriptEnding.connect(listener, 'disconnect');
|
|
function KeyListener(options) {
|
|
assert(typeof options === 'object' && 'text' in options && 'onKeyPressEvent' in options);
|
|
|
|
this._options = options;
|
|
assign(this, {
|
|
modifiers: this._getEventModifiers(options, true)
|
|
}, options);
|
|
this.log = options.log || function log() {
|
|
print('KeyListener | ', [].slice.call(arguments).join(' '));
|
|
};
|
|
this.log('created KeyListener', JSON.stringify(this.text), this.modifiers);
|
|
this.connect();
|
|
}
|
|
KeyListener.prototype = {
|
|
_getEventModifiers: function(event, trueOnly) {
|
|
return '(' + [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ].map(function(mod) {
|
|
var isMod = 'is' + mod,
|
|
value = event[isMod],
|
|
found = (trueOnly ? value : typeof value === 'boolean');
|
|
return found && isMod + ' == ' + value;
|
|
}).filter(Boolean).sort().join(' | ') + ')';
|
|
},
|
|
handleEvent: function(event, target) {
|
|
if (event.text === this.text) {
|
|
var modifiers = this._getEventModifiers(event, true);
|
|
if (modifiers !== this.modifiers) {
|
|
return;
|
|
}
|
|
return this[target](event);
|
|
}
|
|
},
|
|
connect: function() {
|
|
return this.$bindEvents(true);
|
|
},
|
|
disconnect: function() {
|
|
return this.$bindEvents(false);
|
|
},
|
|
$onKeyPressEvent: function(event) {
|
|
return this.handleEvent(event, 'onKeyPressEvent');
|
|
},
|
|
$onKeyReleaseEvent: function(event) {
|
|
return this.handleEvent(event, 'onKeyReleaseEvent');
|
|
},
|
|
$bindEvents: function(connect) {
|
|
if (this.onKeyPressEvent) {
|
|
Controller.keyPressEvent[connect ? 'connect' : 'disconnect'](this, '$onKeyPressEvent');
|
|
}
|
|
if (this.onKeyReleaseEvent) {
|
|
Controller.keyReleaseEvent[connect ? 'connect' : 'disconnect'](this, '$onKeyReleaseEvent');
|
|
}
|
|
Controller[(connect ? 'capture' : 'release') + 'KeyEvents'](this._options);
|
|
}
|
|
};
|
|
|
|
// helper to reload a client script
|
|
reloadClientScript._findRunning = function(filename) {
|
|
return ScriptDiscoveryService.getRunning().filter(function(script) {
|
|
return 0 === script.path.indexOf(filename);
|
|
});
|
|
};
|
|
function reloadClientScript(filename) {
|
|
function log() {
|
|
print('reloadClientScript | ', [].slice.call(arguments).join(' '));
|
|
}
|
|
log('attempting to reload using stopScript(..., true):', filename);
|
|
var result = ScriptDiscoveryService.stopScript(filename, true);
|
|
if (!result) {
|
|
var matches = reloadClientScript._findRunning(filename),
|
|
path = matches[0] && matches[0].path;
|
|
if (path) {
|
|
log('attempting to reload using matched getRunning path: ' + path);
|
|
result = ScriptDiscoveryService.stopScript(path, true);
|
|
}
|
|
}
|
|
log('///result:' + result);
|
|
return result;
|
|
}
|