mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-09 02:42:19 +02:00
Merge pull request #10574 from humbletim/21180
CR #21180 Camera control app
This commit is contained in:
commit
18be917358
18 changed files with 6290 additions and 0 deletions
41
unpublishedScripts/marketplace/camera-move/Eye-Camera.svg
Normal file
41
unpublishedScripts/marketplace/camera-move/Eye-Camera.svg
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 -270 2622 1198" enable-background="new 0 0 2622 1198" xml:space="preserve">
|
||||
<g transform="scale(1,.4546)">
|
||||
<path d="M2591,599c0,0-573.0756,558.7166-1280,558.7166S31,599,31,599S604.0755,40.2834,1311,40.2834S2591,599,2591,599z"/>
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M1507.3644,831.6261c8.1908-85.9868,6.8108-166.8081,5.306-206.6722l-92.7485,160.6452l0.1365,0.2027
|
||||
c-0.0315,0.0212-0.1725,0.1157-0.4128,0.2759l-0.3179,0.5509h-0.5111c-11.4204,7.5681-99.7214,65.3458-204.2842,113.1478
|
||||
c-65.7413,30.0543-125.0349,50.3848-176.2341,60.427c-33.0687,6.4862-62.8685,8.665-89.223,6.5692
|
||||
c93.1363,91.6643,220.923,148.226,361.9247,148.226c38.7316,0,76.463-4.2765,112.7566-12.3666
|
||||
c19.0804-22.5304,35.3513-53.2766,48.5972-91.9321C1488.853,962.5517,1500.6323,902.3027,1507.3644,831.6261z"/>
|
||||
<path fill="#FFFFFF" d="M1202.6725,411.3712h0.5111c11.4207-7.5681,99.7214-65.3457,204.2842-113.1477
|
||||
c65.7412-30.0542,125.0348-50.3848,176.234-60.4269c33.0687-6.4863,62.8685-8.6651,89.223-6.5692
|
||||
C1579.7885,139.563,1452.0017,83.0015,1311,83.0015c-38.7316,0-76.463,4.2764-112.7566,12.3666
|
||||
c-19.0803,22.5305-35.3513,53.2766-48.5973,91.932c-16.499,48.1481-28.2784,108.3973-35.0105,179.0738
|
||||
c-8.1908,85.9868-6.8108,166.8082-5.306,206.6722l92.7484-160.6451l-0.1364-0.2027c0.0314-0.0211,0.1724-0.1157,0.4127-0.2759
|
||||
L1202.6725,411.3712z"/>
|
||||
<path fill="#FFFFFF" d="M1586.8657,253.4136c-49.9471,9.7853-108.0142,29.7088-172.5878,59.2168
|
||||
c-78.5621,35.9-147.8654,77.5058-181.6364,98.741h185.4969l0.1073-0.2195c0.0342,0.0167,0.1865,0.0915,0.4454,0.2195h0.6364
|
||||
l0.2554,0.4426c12.2645,6.1064,106.4518,53.6884,200.1307,120.3414c58.8986,41.9065,106.1522,83.091,140.4484,122.4097
|
||||
c22.2083,25.4601,39.0232,50.2394,50.3865,74.1677c10.7344-41.452,16.45-84.9244,16.45-129.7325
|
||||
c0-134.8296-51.7194-257.5746-136.3927-349.4958C1661.5886,244.2816,1626.8857,245.5726,1586.8657,253.4136z"/>
|
||||
<path fill="#FFFFFF" d="M1748.2195,665.1135c-33.4479-38.3628-79.7356-78.6885-137.5773-119.857
|
||||
c-70.3712-50.0869-141.0546-89.3023-176.3302-107.9312l92.7485,160.6452l0.2438-0.0168c0.0027,0.0379,0.014,0.2072,0.0326,0.4955
|
||||
l0.318,0.551l-0.2555,0.4425c0.844,13.6745,6.7305,119.0341-4.1532,233.4891c-6.8429,71.9608-18.8829,133.4758-35.7858,182.8367
|
||||
c-10.9449,31.963-23.9971,58.9147-39.0378,80.7199c168.875-46.5455,303.0535-176.7669,355.1321-343.2732
|
||||
C1793.5869,725.4113,1775.0889,695.9305,1748.2195,665.1135z"/>
|
||||
<path fill="#FFFFFF" d="M1035.1344,944.5864c49.947-9.7854,108.014-29.7087,172.5876-59.2168
|
||||
c78.5623-35.9,147.8655-77.5058,181.6365-98.741h-185.4971l-0.1073,0.2195c-0.0342-0.0167-0.1865-0.0915-0.4454-0.2195h-0.6364
|
||||
l-0.2554-0.4426c-12.2645-6.1064-106.4518-53.6884-200.1309-120.3412c-58.8984-41.9065-106.152-83.091-140.4482-122.4097
|
||||
c-22.2083-25.46-39.0232-50.2393-50.3865-74.1676C800.7169,510.7194,795.0015,554.192,795.0015,599
|
||||
c0,134.8295,51.7193,257.5745,136.3928,349.4958C960.4114,953.7184,995.1143,952.4273,1035.1344,944.5864z"/>
|
||||
<path fill="#FFFFFF" d="M873.7805,532.8865c33.4479,38.3627,79.7355,78.6885,137.5771,119.857
|
||||
c70.3715,50.0868,141.0547,89.3022,176.3304,107.9312l-92.7487-160.645l-0.2438,0.0167c-0.0026-0.0378-0.014-0.2072-0.0326-0.4954
|
||||
l-0.318-0.551l0.2555-0.4426c-0.8439-13.6744-6.7303-119.034,4.1532-233.4892c6.8429-71.9607,18.8829-133.4758,35.7858-182.8366
|
||||
c10.9451-31.963,23.9971-58.9148,39.0378-80.7198c-168.8749,46.5456-303.0535,176.7669-355.132,343.2733
|
||||
C828.413,472.5887,846.9111,502.0695,873.7805,532.8865z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
133
unpublishedScripts/marketplace/camera-move/_debug.js
Normal file
133
unpublishedScripts/marketplace/camera-move/_debug.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
/* eslint-env jquery, browser */
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
/* global EventBridge: true, PARAMS, signal, assert, log, debugPrint,
|
||||
bridgedSettings, _utils, jquerySettings, */
|
||||
|
||||
// helper functions for debugging and testing the UI in an external web brower
|
||||
var _debug = {
|
||||
handleUncaughtException: function onerror(message, fileName, lineNumber, colNumber, err) {
|
||||
if (message === onerror.lastMessage) {
|
||||
return;
|
||||
}
|
||||
onerror.lastMessage = message;
|
||||
var error = (err || Error.lastError);
|
||||
// var stack = error && error.stack;
|
||||
var output = _utils.normalizeStackTrace(error || { message: message });
|
||||
window.console.error(['window.onerror: ', output, message]); // eslint-disable-line no-console
|
||||
var errorNode = document.querySelector('#errors'),
|
||||
textNode = errorNode && errorNode.querySelector('.output');
|
||||
if (textNode) {
|
||||
textNode.innerText = output;
|
||||
}
|
||||
if (errorNode) {
|
||||
errorNode.style.display = 'block';
|
||||
}
|
||||
if (error){
|
||||
error.onerrored = true;
|
||||
}
|
||||
},
|
||||
loadScriptNodes: function loadScriptNodes(selector) {
|
||||
// scripts are loaded this way to ensure that when the client script refreshes, so are the app's dependencies
|
||||
[].forEach.call(document.querySelectorAll(selector), function(script) {
|
||||
script.parentNode.removeChild(script);
|
||||
if (script.src) {
|
||||
script.src += location.search;
|
||||
}
|
||||
script.type = 'application/javascript';
|
||||
document.write(script.outerHTML);
|
||||
});
|
||||
},
|
||||
|
||||
// TESTING MOCKs
|
||||
openEventBridgeMock: function openEventBridgeMock(onEventBridgeOpened) {
|
||||
var updatedValues = openEventBridgeMock.updatedValues = {};
|
||||
// emulate EventBridge's API
|
||||
EventBridge = {
|
||||
emitWebEvent: signal(function emitWebEvent(message){}),
|
||||
scriptEventReceived: signal(function scriptEventReceived(message){}),
|
||||
};
|
||||
EventBridge.emitWebEvent.connect(onEmitWebEvent);
|
||||
onEventBridgeOpened(EventBridge);
|
||||
assert(!bridgedSettings.onUnhandledMessage);
|
||||
bridgedSettings.onUnhandledMessage = function(msg) {
|
||||
log('bridgedSettings.onUnhandledMessage', msg);
|
||||
return true;
|
||||
};
|
||||
// manually trigger initial bootstrapping responses (that the client script would normally send)
|
||||
bridgedSettings.handleExtraParams({uuid: PARAMS.uuid, ns: PARAMS.ns, extraParams: {
|
||||
mock: true,
|
||||
appVersion: 'browsermock',
|
||||
toggleKey: { text: 'SPACE', isShifted: true },
|
||||
mode: {
|
||||
toolbar: true,
|
||||
browser: true,
|
||||
desktop: true,
|
||||
tablet: /tablet/.test(location) || /android|ipad|iphone/i.test(navigator.userAgent),
|
||||
hmd: /hmd/.test(location),
|
||||
},
|
||||
} });
|
||||
bridgedSettings.setValue('ui-show-advanced-options', true);
|
||||
|
||||
function log(msg) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log.apply(console, ['[mock] ' + msg].concat([].slice.call(arguments,1)));
|
||||
}
|
||||
|
||||
// generate mock data in response to outgoing web page events
|
||||
function onEmitWebEvent(message) {
|
||||
try {
|
||||
var obj = JSON.parse(message);
|
||||
} catch (e) {}
|
||||
if (!obj) {
|
||||
// message isn't JSON so just log it and bail early
|
||||
log('consuming non-callback web event', message);
|
||||
return;
|
||||
}
|
||||
switch (obj.method) {
|
||||
case 'valueUpdated': {
|
||||
log('valueUpdated',obj.params);
|
||||
updatedValues[obj.params[0]] = obj.params[1];
|
||||
return;
|
||||
}
|
||||
case 'Settings.getValue': {
|
||||
var key = obj.params[0];
|
||||
var node = jquerySettings.findNodeByKey(key, true);
|
||||
// log('Settings.getValue.findNodeByKey', key, node);
|
||||
var type = node && (node.dataset.hifiType || node.dataset.type || node.type);
|
||||
switch (type) {
|
||||
case 'hifiButton':
|
||||
case 'hifiCheckbox': {
|
||||
obj.result = /tooltip|advanced-options/i.test(key) || PARAMS.tooltiptest ? true : Math.random() > 0.5;
|
||||
} break;
|
||||
case 'hifiRadioGroup': {
|
||||
var radios = $(node).find('input[type=radio]').toArray();
|
||||
while (Math.random() < 0.9) {
|
||||
radios.push(radios.shift());
|
||||
}
|
||||
obj.result = radios[0].value;
|
||||
} break;
|
||||
case 'hifiSpinner':
|
||||
case 'hifiSlider': {
|
||||
var step = node.step || 1, precision = (1/step).toString().length - 1;
|
||||
var magnitude = node.max || (precision >=1 ? Math.pow(10, precision-1) : 10);
|
||||
obj.result = parseFloat((Math.random() * magnitude).toFixed(precision||1));
|
||||
} break;
|
||||
default: {
|
||||
log('unhandled node type for making dummy data: ' + [key, node && node.type, type, node && node.type] + ' @ ' + (node && node.id));
|
||||
obj.result = updatedValues[key] || false;
|
||||
} break;
|
||||
}
|
||||
debugPrint('mock getValue data %c%s = %c%s', 'color:blue',
|
||||
JSON.stringify(key), 'color:green', JSON.stringify(obj.result));
|
||||
} break;
|
||||
default: {
|
||||
log('ignoring outbound method call', obj);
|
||||
} break;
|
||||
}
|
||||
setTimeout(function() {
|
||||
EventBridge.scriptEventReceived(JSON.stringify(obj));
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
130
unpublishedScripts/marketplace/camera-move/_json-persist.js
Normal file
130
unpublishedScripts/marketplace/camera-move/_json-persist.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
/* eslint-env jquery, browser */
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
/* global _utils, PARAMS, VERSION, signal, assert, log, debugPrint,
|
||||
bridgedSettings, POPUP */
|
||||
|
||||
// JSON export / import helpers proto module
|
||||
var SettingsJSON = (function() {
|
||||
_utils.exists;
|
||||
assert.exists;
|
||||
|
||||
return {
|
||||
setPath: setPath,
|
||||
rollupPaths: rollupPaths,
|
||||
encodeNodes: encodeNodes,
|
||||
exportAll: exportAll,
|
||||
showSettings: showSettings,
|
||||
applyJSON: applyJSON,
|
||||
promptJSON: promptJSON,
|
||||
popupJSON: popupJSON,
|
||||
};
|
||||
|
||||
function encodeNodes(resolver) {
|
||||
return resolver.getAllNodes().reduce((function(out, input, i) {
|
||||
//debugPrint('input['+i+']', input.id);
|
||||
var id = input.id,
|
||||
key = resolver.getKey(id);
|
||||
//debugPrint('toJSON', id, key, input.id);
|
||||
setPath(out, key.split('/'), resolver.getValue(key));
|
||||
return out;
|
||||
}).bind(this), {});
|
||||
}
|
||||
|
||||
function setPath(obj, path, value) {
|
||||
var key = path.pop();
|
||||
obj = path.reduce(function(obj, subkey) {
|
||||
return obj[subkey] = obj[subkey] || {};
|
||||
}, obj);
|
||||
//debugPrint('setPath', key, Object.keys(obj));
|
||||
obj[key] = value;
|
||||
}
|
||||
|
||||
function rollupPaths(obj, output, path) {
|
||||
path = path || [];
|
||||
output = output || {};
|
||||
// log('rollupPaths', Object.keys(obj||{}), Object.keys(output), path);
|
||||
for (var p in obj) {
|
||||
path.push(p);
|
||||
var value = obj[p];
|
||||
if (value && typeof value === 'object') {
|
||||
rollupPaths(obj[p], output, path);
|
||||
} else {
|
||||
output[path.join('/')] = value;
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function exportAll(resolver, name) {
|
||||
var settings = encodeNodes(resolver);
|
||||
Object.keys(settings).forEach(function(prop) {
|
||||
if (typeof settings[prop] === 'object') {
|
||||
_utils.sortedAssign(settings[prop]);
|
||||
}
|
||||
});
|
||||
return {
|
||||
version: VERSION,
|
||||
name: name || undefined,
|
||||
settings: settings,
|
||||
_metadata: { timestamp: new Date(), PARAMS: PARAMS, url: location.href, }
|
||||
};
|
||||
}
|
||||
|
||||
function showSettings(resolver, saveName) {
|
||||
popupJSON(saveName || '(current settings)', Object.assign(exportAll(resolver, saveName), {
|
||||
extraParams: bridgedSettings.extraParams,
|
||||
}));
|
||||
}
|
||||
|
||||
function popupJSON(title, tmp) {
|
||||
var HTML = document.getElementById('POPUP').innerHTML
|
||||
.replace(/\bxx-script\b/g, 'script')
|
||||
.replace('JSON', JSON.stringify(tmp, 0, 2).replace(/\n/g, '<br />'));
|
||||
if (/WebWindowEx/.test(navigator.userAgent) ) {
|
||||
bridgedSettings.sendEvent({
|
||||
method: 'overlayWebWindow',
|
||||
userAgent: navigator.userAgent,
|
||||
options: {
|
||||
title: 'app-camera-move-export' + (title ? '::'+title : ''),
|
||||
content: HTML,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// append a footer to the data URI so it displays cleaner in the built-in browser window that opens
|
||||
var footer = '<\!-- #' + HTML.substr(0,256).replace(/./g,' ') + (title || 'Camera Move Settings');
|
||||
window.open("data:text/html;escape," + encodeURIComponent(HTML) + footer,"app-camera-move-export");
|
||||
}
|
||||
}
|
||||
|
||||
function applyJSON(resolver, name, tmp) {
|
||||
assert(tmp && 'version' in tmp && 'settings' in tmp, 'invalid settings record: ' + JSON.stringify(tmp));
|
||||
var settings = rollupPaths(tmp.settings);
|
||||
for (var p in settings) {
|
||||
if (/^[.]/.test(p)) {
|
||||
continue;
|
||||
}
|
||||
var key = resolver.getId(p, true);
|
||||
if (!key) {
|
||||
log('$applySettings -- skipping unregistered Settings key: ', p);
|
||||
} else {
|
||||
resolver.setValue(p, settings[p], name+'.settings.'+p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function promptJSON() {
|
||||
var json = window.prompt('(paste JSON here)', '');
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
log('parsing json', json);
|
||||
json = JSON.parse(json);
|
||||
} catch (e) {
|
||||
throw new Error('Could not parse pasted JSON: ' + e + '\n\n' + (json||'').replace(/</g,'<'));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
})(this);
|
||||
|
107
unpublishedScripts/marketplace/camera-move/_less-utils.js
Normal file
107
unpublishedScripts/marketplace/camera-move/_less-utils.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
/* eslint-env jquery, browser */
|
||||
/* eslint-disable comma-dangle */
|
||||
/* global PARAMS, signal, assert, log, debugPrint */
|
||||
|
||||
function preconfigureLESS(options) {
|
||||
assert(function assertion() {
|
||||
return options.selector && options.globalVars;
|
||||
});
|
||||
|
||||
options.globalVars = Object.assign(options.globalVars||{}, { hash: '' });
|
||||
Object.assign(options, {
|
||||
errorReporting: Object.assign(function errorReporting(op, e, rootHref) {
|
||||
var type = 'Less-'+e.type+'-Error';
|
||||
var lastResult = errorReporting.lastResult;
|
||||
delete errorReporting.lastResult;
|
||||
if (lastResult && lastResult['#line']) {
|
||||
e.line += lastResult['#line'] - 2;
|
||||
}
|
||||
e.stack = [type+':'+e.message+' at '+e.filename+':'+(e.line+1)]
|
||||
.concat((e.extract||[]).map(function(output, index) {
|
||||
return (index + e.line)+': '+output;
|
||||
})).join('\n\t');
|
||||
_debug.handleUncaughtException(type+': ' + e.message, e.filename, e.line, e.column, {
|
||||
type: type,
|
||||
message: e.message,
|
||||
fileName: e.filename,
|
||||
lineNumber: e.line,
|
||||
stack: e.stack,
|
||||
});
|
||||
}, {
|
||||
lastResult: null,
|
||||
lastSource: null,
|
||||
getLocation: null,
|
||||
$patch: function() {
|
||||
// when errors occur, stash the source and #line {offset} for better error dispay above
|
||||
assert(!this.getLocation);
|
||||
var originalGetLocation = less.utils.getLocation,
|
||||
self = this;
|
||||
less.utils.getLocation = customGetLocation;
|
||||
delete self.$patch; // only need to apply once
|
||||
function customGetLocation(index, stream) {
|
||||
self.lastSource = stream;
|
||||
var result = originalGetLocation.apply(this, arguments);
|
||||
result.source = stream;
|
||||
stream.replace(/#line (\d+)/, function(_, offset) {
|
||||
result['#line'] = parseInt(offset);
|
||||
});
|
||||
return self.lastResult = result;
|
||||
}
|
||||
},
|
||||
}),
|
||||
// poll: 1000,
|
||||
// watch: true,
|
||||
});
|
||||
var lessManager = {
|
||||
options: options,
|
||||
onViewportUpdated: onViewportUpdated,
|
||||
elements: $(options.selector).remove()
|
||||
};
|
||||
|
||||
return lessManager;
|
||||
|
||||
function onViewportUpdated(viewport) {
|
||||
var globalVars = options.globalVars,
|
||||
less = window.less;
|
||||
if (onViewportUpdated.to) {
|
||||
clearTimeout(onViewportUpdated.to);
|
||||
onViewportUpdated.to = 0;
|
||||
} else if (globalVars.hash) {
|
||||
onViewportUpdated.to = setTimeout(onViewportUpdated.bind(this, viewport), 500); // debounce
|
||||
return;
|
||||
}
|
||||
delete globalVars.hash;
|
||||
Object.assign(globalVars, {
|
||||
'inner-width': viewport.inner.width,
|
||||
'inner-height': viewport.inner.height,
|
||||
'client-width': viewport.client.width,
|
||||
'client-height': viewport.client.height,
|
||||
'hash': '',
|
||||
});
|
||||
globalVars.hash = JSON.stringify(JSON.stringify(globalVars,0,2)).replace(/\\n/g , '\\000a');
|
||||
var hash = JSON.stringify(globalVars, 0, 2);
|
||||
debugPrint('onViewportUpdated', JSON.parse(onViewportUpdated.lastHash||'{}')['inner-width'], JSON.parse(hash)['inner-width']);
|
||||
if (onViewportUpdated.lastHash !== hash) {
|
||||
debugPrint('updating lessVars', 'less.modifyVars:' + typeof less.modifyVars, JSON.stringify(globalVars, 0, 2));
|
||||
// dump less variables if lessDebug=true was in the url
|
||||
PARAMS.lessDebug && $('#errors').show().html("<pre>").children(0).text(hash);
|
||||
|
||||
// patch less with absolute line number reporting
|
||||
options.errorReporting && options.errorReporting.$patch && options.errorReporting.$patch();
|
||||
|
||||
// to recompile inline styles (in response to onresize or when developing),
|
||||
// a fresh copy of the source nodes gets swapped-in
|
||||
var newNodes = lessManager.elements.clone().appendTo(document.body);
|
||||
// note: refresh(reload, modifyVars, clearFileCache)
|
||||
less.refresh(false, globalVars).then(function(result) {
|
||||
debugPrint('less.refresh completed OK', result);
|
||||
})['catch'](function(err) {
|
||||
log('less ERROR:', err);
|
||||
});
|
||||
var oldNodes = onViewportUpdated.lastNodes;
|
||||
oldNodes && oldNodes.remove();
|
||||
onViewportUpdated.lastNodes = newNodes;
|
||||
}
|
||||
onViewportUpdated.lastHash = hash;
|
||||
}
|
||||
}
|
169
unpublishedScripts/marketplace/camera-move/_tooltips.js
Normal file
169
unpublishedScripts/marketplace/camera-move/_tooltips.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
/* eslint-env jquery, browser */
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
/* global PARAMS, signal, assert, log, debugPrint */
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// manage jquery-tooltipster hover tooltips
|
||||
var TooltipManager = (function(global) {
|
||||
Object.assign(TooltipManager, {
|
||||
BASECONFIG: {
|
||||
theme: ['tooltipster-noir'],
|
||||
side: ['right','top','bottom', 'left'],
|
||||
updateAnimation: 'scale',
|
||||
delay: [750, 1000],
|
||||
distance: { right: 24, left: 8, top: 8, bottom: 8 },
|
||||
contentAsHTML: true,
|
||||
},
|
||||
});
|
||||
|
||||
function TooltipManager(options) {
|
||||
assert(options.elements && options.tooltips, 'TooltipManager constructor expects .elements and .tooltips');
|
||||
Object.assign(this, {
|
||||
instances: [],
|
||||
options: options,
|
||||
config: Object.assign({}, TooltipManager.BASECONFIG, {
|
||||
trigger: !options.testmode ? 'hover' : 'click',
|
||||
interactive: options.testmode,
|
||||
minWidth: options.viewport && options.viewport.min.width,
|
||||
maxWidth: options.viewport && options.viewport.max.width,
|
||||
}),
|
||||
});
|
||||
options.enabled && this.initialize();
|
||||
}
|
||||
|
||||
TooltipManager.prototype = {
|
||||
constructor: TooltipManager,
|
||||
initialize: function() {
|
||||
var options = this.options,
|
||||
_config = this.config,
|
||||
_self = this,
|
||||
candidates = $(options.elements);
|
||||
|
||||
candidates.add($('button')).each(function() {
|
||||
var id = this.id,
|
||||
input = $(this),
|
||||
tip = options.tooltips[id] || options.tooltips[input.data('for')];
|
||||
|
||||
var alreadyTipped = input.is('.tooltipstered') || input.closest('.tooltipstered').get(0);
|
||||
if (alreadyTipped || !tip) {
|
||||
return !tip && _debugPrint('!tooltippable -- missing tooltip for ' + (id || input.data('for') || input.text()));
|
||||
}
|
||||
var config = Object.assign({ content: tip }, _config);
|
||||
|
||||
function mergeConfig() {
|
||||
var attr = $(this).attr('data-tooltipster'),
|
||||
object = $(this).data('tooltipster');
|
||||
typeof object === 'object' && Object.assign(config, object);
|
||||
attr && Object.assign(config, JSON.parse(attr));
|
||||
}
|
||||
try {
|
||||
input.parents(':data(tooltipster),[data-tooltipster]').each(mergeConfig);
|
||||
input.each(mergeConfig); // prioritize own settings
|
||||
} catch(e) {
|
||||
console.error('error extracting tooltipster data:' + [e, id]);
|
||||
}
|
||||
|
||||
var target = $(input.closest('.tooltip-target').get(0) ||
|
||||
(input.is('input') && input) || null);
|
||||
|
||||
assert(target && target[0] && tip);
|
||||
debugPrint('binding tooltip', config, target[0].nodeName, id || target[0]);
|
||||
var instance = target.tooltipster(config)
|
||||
.tooltipster('instance');
|
||||
|
||||
instance.on('close', function(event) {
|
||||
if (options.keepopen === target) {
|
||||
debugPrint(event.type, 'canceling close keepopen === target', id);
|
||||
event.stop();
|
||||
options.keepopen = null;
|
||||
}
|
||||
});
|
||||
instance.on('before', function(event) {
|
||||
debugPrint(event.type, 'before', event);
|
||||
!options.testmode && _self.closeAll();
|
||||
!options.enabled && event.stop();
|
||||
});
|
||||
target.find(':focusable, input, [tabindex], button, .control')
|
||||
.add(target).add(input)
|
||||
.add(input.closest(':focusable, input, [tabindex]'))
|
||||
.on({
|
||||
click: function(evt) {
|
||||
if (input.is('button')) {
|
||||
return setTimeout(instance.close.bind(instance,null),50);
|
||||
}
|
||||
options.keepopen = target;
|
||||
},
|
||||
focus: instance.open.bind(instance, null),
|
||||
blur: function(evt) {
|
||||
instance.close(); _self.openFocusedTooltip();
|
||||
},
|
||||
});
|
||||
_self.instances.push(instance);
|
||||
});
|
||||
return this.instances;
|
||||
},
|
||||
openFocusedTooltip: function() {
|
||||
if (!this.options.enabled) {
|
||||
return;
|
||||
}
|
||||
setTimeout(function() {
|
||||
if (!document.activeElement || document.activeElement === document.body ||
|
||||
!$(document.activeElement).closest('section')) {
|
||||
return;
|
||||
}
|
||||
var tip = $([])
|
||||
.add($(document.activeElement))
|
||||
.add($(document.activeElement).find('.tooltipstered'))
|
||||
.add($(document.activeElement).closest('.tooltipstered'))
|
||||
.filter('.tooltipstered');
|
||||
if (tip.is('.tooltipstered')) {
|
||||
// log('opening focused tooltip', tip.length, tip[0].id);
|
||||
tip.tooltipster('open');
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
rapidClose: function(instance, reopen) {
|
||||
if (!instance.status().open) {
|
||||
return;
|
||||
}
|
||||
instance.elementTooltip() && $(instance.elementTooltip()).hide();
|
||||
instance.close(function() {
|
||||
reopen && instance.open();
|
||||
});
|
||||
},
|
||||
openAll: function() {
|
||||
$('.tooltipstered').tooltipster('open');
|
||||
},
|
||||
closeAll: function() {
|
||||
$.tooltipster.instances().forEach(function(instance) {
|
||||
this.rapidClose(instance);
|
||||
}.bind(this));
|
||||
},
|
||||
updateViewport: function(viewport) {
|
||||
var options = {
|
||||
minWidth: viewport.min.width,
|
||||
maxWidth: viewport.max.width,
|
||||
};
|
||||
Object.assign(this.config, options);
|
||||
$.tooltipster.setDefaults(options);
|
||||
debugPrint('updating tooltipster options', JSON.stringify(options));
|
||||
$.tooltipster.instances().forEach(function(instance) {
|
||||
instance.option('minWidth', options.minWidth);
|
||||
instance.option('maxWidth', options.maxWidth);
|
||||
this.rapidClose(instance, instance.status().open);
|
||||
}.bind(this));
|
||||
},
|
||||
enable: function() {
|
||||
this.options.enabled = true;
|
||||
if (this.options.testmode) {
|
||||
this.openAll();
|
||||
}
|
||||
},
|
||||
disable: function() {
|
||||
this.options.enabled = false;
|
||||
this.closeAll();
|
||||
},
|
||||
};// prototype
|
||||
|
||||
return TooltipManager;
|
||||
})(this);
|
644
unpublishedScripts/marketplace/camera-move/app-camera-move.js
Normal file
644
unpublishedScripts/marketplace/camera-move/app-camera-move.js
Normal file
|
@ -0,0 +1,644 @@
|
|||
// app-camera-move.js
|
||||
//
|
||||
// Created by Timothy Dedischew on 05/05/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
|
||||
//
|
||||
// This Client script sets up the Camera Control Tablet App, which can be used to configure and
|
||||
// drive your avatar with easing/smoothing movement constraints for a less jittery filming experience.
|
||||
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
"use strict";
|
||||
|
||||
var VERSION = '0.0.1',
|
||||
NAMESPACE = 'app-camera-move',
|
||||
APP_HTML_URL = Script.resolvePath('app.html'),
|
||||
BUTTON_CONFIG = {
|
||||
text: '\nCam Drive',
|
||||
icon: Script.resolvePath('Eye-Camera.svg'),
|
||||
},
|
||||
DEFAULT_TOGGLE_KEY = { text: 'SPACE' };
|
||||
|
||||
var MINIMAL_CURSOR_SCALE = 0.5,
|
||||
FILENAME = Script.resolvePath(''),
|
||||
WANT_DEBUG = Settings.getValue(NAMESPACE + '/debug', false)
|
||||
EPSILON = 1e-6;
|
||||
|
||||
function log() {
|
||||
print( NAMESPACE + ' | ' + [].slice.call(arguments).join(' ') );
|
||||
}
|
||||
|
||||
var require = Script.require,
|
||||
debugPrint = function(){},
|
||||
_debugChannel = NAMESPACE + '.stats',
|
||||
overlayDebugOutput = function(){};
|
||||
|
||||
if (WANT_DEBUG) {
|
||||
log('WANT_DEBUG is true; instrumenting debug support', WANT_DEBUG);
|
||||
_instrumentDebug();
|
||||
}
|
||||
|
||||
var _utils = require('./modules/_utils.js'),
|
||||
assert = _utils.assert,
|
||||
CustomSettingsApp = require('./modules/custom-settings-app/CustomSettingsApp.js'),
|
||||
movementUtils = require('./modules/movement-utils.js?'+ +new Date),
|
||||
configUtils = require('./modules/config-utils.js'),
|
||||
AvatarUpdater = require('./avatar-updater.js');
|
||||
|
||||
|
||||
Object.assign = Object.assign || _utils.assign;
|
||||
|
||||
var cameraControls, eventMapper, cameraConfig, applicationConfig;
|
||||
|
||||
var DEFAULTS = {
|
||||
'namespace': NAMESPACE,
|
||||
'debug': WANT_DEBUG,
|
||||
'jitter-test': false,
|
||||
'camera-move-enabled': false,
|
||||
'thread-update-mode': movementUtils.CameraControls.SCRIPT_UPDATE,
|
||||
'fps': 90,
|
||||
'drive-mode': movementUtils.DriveModes.MOTOR,
|
||||
'use-head': true,
|
||||
'stay-grounded': true,
|
||||
'prevent-roll': true,
|
||||
'constant-delta-time': false,
|
||||
'minimal-cursor': false,
|
||||
'normalize-inputs': false,
|
||||
'enable-mouse-smooth': true,
|
||||
'translation-max-velocity': 5.50,
|
||||
'translation-ease-in': 1.25,
|
||||
'translation-ease-out': 5.50,
|
||||
'rotation-max-velocity': 90.00,
|
||||
'rotation-ease-in': 1.00,
|
||||
'rotation-ease-out': 5.50,
|
||||
'rotation-x-speed': 45,
|
||||
'rotation-y-speed': 60,
|
||||
'rotation-z-speed': 1,
|
||||
'mouse-multiplier': 1.0,
|
||||
'keyboard-multiplier': 1.0,
|
||||
|
||||
'ui-enable-tooltips': true,
|
||||
'ui-show-advanced-options': false,
|
||||
|
||||
'Avatar/Draw Mesh': true,
|
||||
'Scene/shouldRenderEntities': true,
|
||||
'Scene/shouldRenderAvatars': true,
|
||||
'Avatar/Show My Eye Vectors': false,
|
||||
'Avatar/Show Other Eye Vectors': false,
|
||||
};
|
||||
|
||||
// map setting names to/from corresponding Menu and API properties
|
||||
var APPLICATION_SETTINGS = {
|
||||
'Avatar/Enable Avatar Collisions': {
|
||||
menu: 'Avatar > Enable Avatar Collisions',
|
||||
object: [ MyAvatar, 'collisionsEnabled' ],
|
||||
},
|
||||
'Avatar/Draw Mesh': {
|
||||
menu: 'Developer > Draw Mesh',
|
||||
object: [ MyAvatar, 'getEnableMeshVisible', 'setEnableMeshVisible' ],
|
||||
},
|
||||
'Avatar/Show My Eye Vectors': { menu: 'Developer > Show My Eye Vectors' },
|
||||
'Avatar/Show Other Eye Vectors': { menu: 'Developer > Show Other Eye Vectors' },
|
||||
'Avatar/useSnapTurn': { object: [ MyAvatar, 'getSnapTurn', 'setSnapTurn' ] },
|
||||
'Avatar/lookAtSnappingEnabled': 'lookAtSnappingEnabled' in MyAvatar && {
|
||||
menu: 'Developer > Enable LookAt Snapping',
|
||||
object: [ MyAvatar, 'lookAtSnappingEnabled' ]
|
||||
},
|
||||
'Scene/shouldRenderEntities': { object: [ Scene, 'shouldRenderEntities' ] },
|
||||
'Scene/shouldRenderAvatars': { object: [ Scene, 'shouldRenderAvatars' ] },
|
||||
'camera-move-enabled': {
|
||||
get: function() {
|
||||
return cameraControls && cameraControls.enabled;
|
||||
},
|
||||
set: function(nv) {
|
||||
cameraControls.setEnabled(!!nv);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var DEBUG_INFO = {
|
||||
// these values are also sent to the tablet app after EventBridge initialization
|
||||
appVersion: VERSION,
|
||||
utilsVersion: _utils.version,
|
||||
movementVersion: movementUtils.version,
|
||||
configVersion: configUtils.version,
|
||||
clientScript: Script.resolvePath(''),
|
||||
MyAvatar: {
|
||||
supportsPitchSpeed: 'pitchSpeed' in MyAvatar,
|
||||
supportsYawSpeed: 'yawSpeed' in MyAvatar,
|
||||
supportsLookAtSnappingEnabled: 'lookAtSnappingEnabled' in MyAvatar,
|
||||
},
|
||||
Reticle: {
|
||||
supportsScale: 'scale' in Reticle,
|
||||
},
|
||||
protocolVersion: location.protocolVersion,
|
||||
};
|
||||
|
||||
var globalState = {
|
||||
// cached values from the last animation frame
|
||||
previousValues: {
|
||||
reset: function() {
|
||||
this.pitchYawRoll = Vec3.ZERO;
|
||||
this.thrust = Vec3.ZERO;
|
||||
},
|
||||
},
|
||||
|
||||
// batch updates to MyAvatar/Camera properties (submitting together seems to help reduce jitter)
|
||||
pendingChanges: _utils.DeferredUpdater.createGroup({
|
||||
Camera: Camera,
|
||||
MyAvatar: MyAvatar,
|
||||
}, { dedupe: false }),
|
||||
|
||||
// current input controls' effective velocities
|
||||
currentVelocities: new movementUtils.VelocityTracker({
|
||||
translation: Vec3.ZERO,
|
||||
rotation: Vec3.ZERO,
|
||||
zoom: Vec3.ZERO,
|
||||
}),
|
||||
};
|
||||
|
||||
function main() {
|
||||
log('initializing...', VERSION);
|
||||
|
||||
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
||||
button = tablet.addButton(BUTTON_CONFIG);
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
tablet.removeButton(button);
|
||||
button = null;
|
||||
});
|
||||
|
||||
// track runtime state (applicationConfig) and Settings state (cameraConfig)
|
||||
applicationConfig = new configUtils.ApplicationConfig({
|
||||
namespace: DEFAULTS.namespace,
|
||||
config: APPLICATION_SETTINGS,
|
||||
});
|
||||
cameraConfig = new configUtils.SettingsConfig({
|
||||
namespace: DEFAULTS.namespace,
|
||||
defaultValues: DEFAULTS,
|
||||
});
|
||||
|
||||
var toggleKey = DEFAULT_TOGGLE_KEY;
|
||||
if (cameraConfig.getValue('toggle-key')) {
|
||||
try {
|
||||
toggleKey = JSON.parse(cameraConfig.getValue('toggle-key'));
|
||||
} catch (e) {}
|
||||
}
|
||||
// monitor configuration changes / keep tablet app up-to-date
|
||||
var MONITOR_INTERVAL_MS = 1000;
|
||||
_startConfigationMonitor(applicationConfig, cameraConfig, MONITOR_INTERVAL_MS);
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// set up the tablet app
|
||||
log('APP_HTML_URL', APP_HTML_URL);
|
||||
var settingsApp = new CustomSettingsApp({
|
||||
namespace: cameraConfig.namespace,
|
||||
uuid: cameraConfig.uuid,
|
||||
settingsAPI: cameraConfig,
|
||||
url: APP_HTML_URL,
|
||||
tablet: tablet,
|
||||
extraParams: Object.assign({
|
||||
toggleKey: toggleKey,
|
||||
}, getSystemMetadata(), DEBUG_INFO),
|
||||
debug: WANT_DEBUG > 1,
|
||||
});
|
||||
Script.scriptEnding.connect(settingsApp, 'cleanup');
|
||||
settingsApp.valueUpdated.connect(function(key, value, oldValue, origin) {
|
||||
log('settingsApp.valueUpdated: '+ key + ' = ' + JSON.stringify(value) + ' (was: ' + JSON.stringify(oldValue) + ')');
|
||||
if (/tablet/i.test(origin)) {
|
||||
// apply relevant settings immediately if changed from the tablet UI
|
||||
if (applicationConfig.applyValue(key, value, origin)) {
|
||||
log('settingsApp applied immediate setting', key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// process custom eventbridge messages
|
||||
settingsApp.onUnhandledMessage = function(msg) {
|
||||
switch (msg.method) {
|
||||
case 'window.close': {
|
||||
this.toggle(false);
|
||||
} break;
|
||||
case 'reloadClientScript': {
|
||||
log('reloadClientScript...');
|
||||
_utils.reloadClientScript(FILENAME);
|
||||
} break;
|
||||
case 'resetSensors': {
|
||||
Menu.triggerOption('Reset Sensors');
|
||||
Script.setTimeout(function() {
|
||||
MyAvatar.bodyPitch = 0;
|
||||
MyAvatar.bodyRoll = 0;
|
||||
MyAvatar.orientation = Quat.cancelOutRollAndPitch(MyAvatar.orientation);
|
||||
}, 500);
|
||||
} break;
|
||||
case 'reset': {
|
||||
var resetValues = {};
|
||||
// maintain current value of 'show advanced' so user can observe any advanced settings being reset
|
||||
var showAdvancedKey = cameraConfig.resolve('ui-show-advanced-options');
|
||||
resetValues[showAdvancedKey] = cameraConfig.getValue(showAdvancedKey);
|
||||
Object.keys(DEFAULTS).reduce(function(out, key) {
|
||||
var resolved = cameraConfig.resolve(key);
|
||||
out[resolved] = resolved in out ? out[resolved] : DEFAULTS[key];
|
||||
return out;
|
||||
}, resetValues);
|
||||
Object.keys(applicationConfig.config).reduce(function(out, key) {
|
||||
var resolved = applicationConfig.resolve(key);
|
||||
out[resolved] = resolved in out ? out[resolved] : applicationConfig.getValue(key);
|
||||
return out;
|
||||
}, resetValues);
|
||||
log('restting to system defaults:', JSON.stringify(resetValues, 0, 2));
|
||||
for (var p in resetValues) {
|
||||
var value = resetValues[p];
|
||||
applicationConfig.applyValue(p, value, 'reset');
|
||||
cameraConfig.setValue(p, value);
|
||||
}
|
||||
} break;
|
||||
default: {
|
||||
log('onUnhandledMessage', JSON.stringify(msg,0,2));
|
||||
} break;
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// set up the keyboard/mouse/controller input state manager
|
||||
eventMapper = new movementUtils.MovementEventMapper({
|
||||
namespace: DEFAULTS.namespace,
|
||||
mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'),
|
||||
mouseMultiplier: cameraConfig.getValue('mouse-multiplier'),
|
||||
keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'),
|
||||
eventFilter: function eventFilter(from, event, defaultFilter) {
|
||||
var result = defaultFilter(from, event),
|
||||
driveKeyName = event.driveKeyName;
|
||||
if (!result || !driveKeyName) {
|
||||
if (from === 'Keyboard.RightMouseButton') {
|
||||
// let the app know when the user is mouse looking
|
||||
settingsApp.syncValue('Keyboard.RightMouseButton', event.actionValue, 'eventFilter');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (cameraConfig.getValue('normalize-inputs')) {
|
||||
result = _utils.sign(result);
|
||||
}
|
||||
if (from === 'Actions.Pitch') {
|
||||
result *= cameraConfig.getFloat('rotation-x-speed');
|
||||
} else if (from === 'Actions.Yaw') {
|
||||
result *= cameraConfig.getFloat('rotation-y-speed');
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
Script.scriptEnding.connect(eventMapper, 'disable');
|
||||
// keep track of these changes live so the controller mapping can be kept in sync
|
||||
applicationConfig.register({
|
||||
'enable-mouse-smooth': { object: [ eventMapper.options, 'mouseSmooth' ] },
|
||||
'keyboard-multiplier': { object: [ eventMapper.options, 'keyboardMultiplier' ] },
|
||||
'mouse-multiplier': { object: [ eventMapper.options, 'mouseMultiplier' ] },
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// set up the top-level camera controls manager / animator
|
||||
var avatarUpdater = new AvatarUpdater({
|
||||
debugChannel: _debugChannel,
|
||||
globalState: globalState,
|
||||
getCameraMovementSettings: getCameraMovementSettings,
|
||||
getMovementState: _utils.bind(eventMapper, 'getState'),
|
||||
});
|
||||
cameraControls = new movementUtils.CameraControls({
|
||||
namespace: DEFAULTS.namespace,
|
||||
update: avatarUpdater,
|
||||
threadMode: cameraConfig.getValue('thread-update-mode'),
|
||||
fps: cameraConfig.getValue('fps'),
|
||||
getRuntimeSeconds: _utils.getRuntimeSeconds,
|
||||
});
|
||||
Script.scriptEnding.connect(cameraControls, 'disable');
|
||||
applicationConfig.register({
|
||||
'thread-update-mode': { object: [ cameraControls, 'threadMode' ] },
|
||||
'fps': { object: [ cameraControls, 'fps' ] },
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// set up SPACEBAR for toggling camera movement mode
|
||||
var spacebar = new _utils.KeyListener(Object.assign(toggleKey, {
|
||||
onKeyPressEvent: function(event) {
|
||||
cameraControls.setEnabled(!cameraControls.enabled);
|
||||
},
|
||||
}));
|
||||
Script.scriptEnding.connect(spacebar, 'disconnect');
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// set up ESC for resetting all drive key states
|
||||
Script.scriptEnding.connect(new _utils.KeyListener({
|
||||
text: 'ESC',
|
||||
onKeyPressEvent: function(event) {
|
||||
if (cameraControls.enabled) {
|
||||
log('ESC pressed -- resetting drive keys:', JSON.stringify({
|
||||
virtualDriveKeys: eventMapper.states,
|
||||
movementState: eventMapper.getState(),
|
||||
}, 0, 2));
|
||||
eventMapper.states.reset();
|
||||
MyAvatar.velocity = Vec3.ZERO;
|
||||
MyAvatar.angularVelocity = Vec3.ZERO;
|
||||
}
|
||||
},
|
||||
}), 'disconnect');
|
||||
|
||||
// set up the tablet button to toggle the UI display
|
||||
button.clicked.connect(settingsApp, function(enable) {
|
||||
Object.assign(this.extraParams, getSystemMetadata());
|
||||
button.editProperties({ text: '(opening)' + BUTTON_CONFIG.text, isActive: true });
|
||||
this.toggle(enable);
|
||||
});
|
||||
|
||||
settingsApp.isActiveChanged.connect(function(isActive) {
|
||||
updateButtonText();
|
||||
if (Overlays.getOverlayType(HMD.tabletScreenID)) {
|
||||
var fromMode = Overlays.getProperty(HMD.tabletScreenID, 'inputMode'),
|
||||
inputMode = isActive ? "Mouse" : "Touch";
|
||||
log('switching HMD.tabletScreenID from inputMode', fromMode, 'to', inputMode);
|
||||
Overlays.editOverlay(HMD.tabletScreenID, { inputMode: inputMode });
|
||||
}
|
||||
});
|
||||
|
||||
cameraControls.modeChanged.connect(onCameraModeChanged);
|
||||
|
||||
function updateButtonText() {
|
||||
var lines = [
|
||||
settingsApp.isActive ? '(app open)' : '',
|
||||
cameraControls.enabled ? (avatarUpdater.update.momentaryFPS||0).toFixed(2) + 'fps' : BUTTON_CONFIG.text.trim()
|
||||
];
|
||||
button && button.editProperties({ text: lines.join('\n') });
|
||||
}
|
||||
|
||||
var fpsTimeout = 0;
|
||||
cameraControls.enabledChanged.connect(function(enabled) {
|
||||
log('enabledChanged', enabled);
|
||||
button && button.editProperties({ isActive: enabled });
|
||||
if (enabled) {
|
||||
onCameraControlsEnabled();
|
||||
fpsTimeout = Script.setInterval(updateButtonText, 1000);
|
||||
} else {
|
||||
if (fpsTimeout) {
|
||||
Script.clearInterval(fpsTimeout);
|
||||
fpsTimeout = 0;
|
||||
}
|
||||
eventMapper.disable();
|
||||
avatarUpdater._resetMyAvatarMotor({ MyAvatar: MyAvatar });
|
||||
updateButtonText();
|
||||
if (settingsApp.isActive) {
|
||||
settingsApp.syncValue('Keyboard.RightMouseButton', false, 'cameraControls.disabled');
|
||||
}
|
||||
}
|
||||
overlayDebugOutput.overlayID && Overlays.editOverlay(overlayDebugOutput.overlayID, { visible: enabled });
|
||||
});
|
||||
|
||||
// when certain settings change we need to reset the drive systems
|
||||
var resetIfChanged = [
|
||||
'minimal-cursor', 'drive-mode', 'fps', 'thread-update-mode',
|
||||
'mouse-multiplier', 'keyboard-multiplier',
|
||||
'enable-mouse-smooth', 'constant-delta-time',
|
||||
].filter(Boolean).map(_utils.bind(cameraConfig, 'resolve'));
|
||||
|
||||
cameraConfig.valueUpdated.connect(function(key, value, oldValue, origin) {
|
||||
var triggerReset = !!~resetIfChanged.indexOf(key);
|
||||
log('cameraConfig.valueUpdated: ' + key + ' = ' + JSON.stringify(value), '(was:' + JSON.stringify(oldValue) + ')',
|
||||
'triggerReset: ' + triggerReset);
|
||||
|
||||
if (/tablet/i.test(origin)) {
|
||||
if (applicationConfig.applyValue(key, value, origin)) {
|
||||
log('cameraConfig applied immediate setting', key, value);
|
||||
}
|
||||
|
||||
}
|
||||
triggerReset && cameraControls.reset();
|
||||
});
|
||||
|
||||
if (cameraConfig.getValue('camera-move-enabled')) {
|
||||
cameraControls.enable();
|
||||
}
|
||||
|
||||
log('DEFAULTS', JSON.stringify(DEFAULTS, 0, 2));
|
||||
} // main()
|
||||
|
||||
function onCameraControlsEnabled() {
|
||||
log('onCameraControlsEnabled');
|
||||
globalState.previousValues.reset();
|
||||
globalState.currentVelocities.reset();
|
||||
globalState.pendingChanges.reset();
|
||||
eventMapper.enable();
|
||||
if (cameraConfig.getValue('minimal-cursor')) {
|
||||
Reticle.scale = MINIMAL_CURSOR_SCALE;
|
||||
}
|
||||
log('cameraConfig', JSON.stringify({
|
||||
cameraConfig: getCameraMovementSettings(),
|
||||
}));
|
||||
}
|
||||
|
||||
// reset orientation-related values when the Camera.mode changes
|
||||
function onCameraModeChanged(mode, oldMode) {
|
||||
globalState.pendingChanges.reset();
|
||||
globalState.previousValues.reset();
|
||||
eventMapper.reset();
|
||||
var preventRoll = cameraConfig.getValue('prevent-roll');
|
||||
var avatarOrientation = cameraConfig.getValue('use-head') ? MyAvatar.headOrientation : MyAvatar.orientation;
|
||||
if (preventRoll) {
|
||||
avatarOrientation = Quat.cancelOutRollAndPitch(avatarOrientation);
|
||||
}
|
||||
switch (Camera.mode) {
|
||||
case 'mirror':
|
||||
case 'entity':
|
||||
case 'independent':
|
||||
globalState.currentVelocities.reset();
|
||||
break;
|
||||
default:
|
||||
Camera.position = MyAvatar.position;
|
||||
Camera.orientation = avatarOrientation;
|
||||
break;
|
||||
}
|
||||
MyAvatar.orientation = avatarOrientation;
|
||||
if (preventRoll) {
|
||||
MyAvatar.headPitch = MyAvatar.headRoll = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// consolidate and normalize cameraConfig settings
|
||||
function getCameraMovementSettings() {
|
||||
return {
|
||||
epsilon: EPSILON,
|
||||
debug: cameraConfig.getValue('debug'),
|
||||
jitterTest: cameraConfig.getValue('jitter-test'),
|
||||
driveMode: cameraConfig.getValue('drive-mode'),
|
||||
threadMode: cameraConfig.getValue('thread-update-mode'),
|
||||
fps: cameraConfig.getValue('fps'),
|
||||
useHead: cameraConfig.getValue('use-head'),
|
||||
stayGrounded: cameraConfig.getValue('stay-grounded'),
|
||||
preventRoll: cameraConfig.getValue('prevent-roll'),
|
||||
useConstantDeltaTime: cameraConfig.getValue('constant-delta-time'),
|
||||
|
||||
collisionsEnabled: applicationConfig.getValue('Avatar/Enable Avatar Collisions'),
|
||||
mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'),
|
||||
mouseMultiplier: cameraConfig.getValue('mouse-multiplier'),
|
||||
keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'),
|
||||
|
||||
rotation: _getEasingGroup(cameraConfig, 'rotation'),
|
||||
translation: _getEasingGroup(cameraConfig, 'translation'),
|
||||
zoom: _getEasingGroup(cameraConfig, 'zoom'),
|
||||
};
|
||||
|
||||
// extract a single easing group (translation, rotation, or zoom) from cameraConfig
|
||||
function _getEasingGroup(cameraConfig, group) {
|
||||
var multiplier = 1.0;
|
||||
if (group === 'zoom') {
|
||||
// BoomIn / TranslateCameraZ support is only partially plumbed -- for now use scaled translation easings
|
||||
group = 'translation';
|
||||
multiplier = 0.001;
|
||||
}
|
||||
return {
|
||||
easeIn: cameraConfig.getFloat(group + '-ease-in'),
|
||||
easeOut: cameraConfig.getFloat(group + '-ease-out'),
|
||||
maxVelocity: multiplier * cameraConfig.getFloat(group + '-max-velocity'),
|
||||
speed: Vec3.multiply(multiplier, {
|
||||
x: cameraConfig.getFloat(group + '-x-speed'),
|
||||
y: cameraConfig.getFloat(group + '-y-speed'),
|
||||
z: cameraConfig.getFloat(group + '-z-speed')
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// monitor and sync Application state -> Settings values
|
||||
function _startConfigationMonitor(applicationConfig, cameraConfig, interval) {
|
||||
return Script.setInterval(function monitor() {
|
||||
var settingNames = Object.keys(applicationConfig.config);
|
||||
settingNames.forEach(function(key) {
|
||||
applicationConfig.resyncValue(key); // align Menus <=> APIs
|
||||
var value = cameraConfig.getValue(key),
|
||||
appValue = applicationConfig.getValue(key);
|
||||
if (appValue !== undefined && String(appValue) !== String(value)) {
|
||||
log('applicationConfig -> cameraConfig',
|
||||
key, [typeof appValue, appValue], '(was:'+[typeof value, value]+')');
|
||||
cameraConfig.setValue(key, appValue); // align Application <=> Settings
|
||||
}
|
||||
});
|
||||
}, interval);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// DEBUG overlay support (enable by setting app-camera-move/debug = true in settings
|
||||
// ----------------------------------------------------------------------------
|
||||
function _instrumentDebug() {
|
||||
debugPrint = log;
|
||||
var cacheBuster = '?' + new Date().getTime().toString(36);
|
||||
require = Script.require(Script.resolvePath('./modules/_utils.js') + cacheBuster).makeDebugRequire(Script.resolvePath('.'));
|
||||
APP_HTML_URL += cacheBuster;
|
||||
overlayDebugOutput = _createOverlayDebugOutput({
|
||||
lineHeight: 12,
|
||||
font: { size: 12 },
|
||||
width: 250, height: 800 });
|
||||
// auto-disable camera move mode when debugging
|
||||
Script.scriptEnding.connect(function() {
|
||||
cameraConfig && cameraConfig.setValue('camera-move-enabled', false);
|
||||
});
|
||||
}
|
||||
|
||||
function _fixedPrecisionStringifiyFilter(key, value, object) {
|
||||
if (typeof value === 'object' && value && 'w' in value) {
|
||||
return Quat.safeEulerAngles(value);
|
||||
} else if (typeof value === 'number') {
|
||||
return value.toFixed(4)*1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function _createOverlayDebugOutput(options) {
|
||||
options = require('./modules/_utils.js').assign({
|
||||
x: 0, y: 0, width: 500, height: 800, visible: false
|
||||
}, options || {});
|
||||
options.lineHeight = options.lineHeight || Math.round(options.height / 36);
|
||||
options.font = options.font || { size: Math.floor(options.height / 36) };
|
||||
overlayDebugOutput.overlayID = Overlays.addOverlay('text', options);
|
||||
|
||||
Messages.subscribe(_debugChannel);
|
||||
Messages.messageReceived.connect(onMessageReceived);
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
Overlays.deleteOverlay(overlayDebugOutput.overlayID);
|
||||
Messages.unsubscribe(_debugChannel);
|
||||
Messages.messageReceived.disconnect(onMessageReceived);
|
||||
});
|
||||
function overlayDebugOutput(output) {
|
||||
var text = JSON.stringify(output, _fixedPrecisionStringifiyFilter, 2);
|
||||
if (text !== overlayDebugOutput.lastText) {
|
||||
overlayDebugOutput.lastText = text;
|
||||
Overlays.editOverlay(overlayDebugOutput.overlayID, { text: text });
|
||||
}
|
||||
}
|
||||
function onMessageReceived(channel, message, ssend, local) {
|
||||
if (local && channel === _debugChannel) {
|
||||
overlayDebugOutput(JSON.parse(message));
|
||||
}
|
||||
}
|
||||
return overlayDebugOutput;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
_patchCameraModeSetting();
|
||||
function _patchCameraModeSetting() {
|
||||
// FIXME: looks like the Camera API suffered a regression where Camera.mode = 'first person' or 'third person'
|
||||
// no longer works from the API; setting via Menu items still seems to work though.
|
||||
Camera.$setModeString = Camera.$setModeString || function(mode) {
|
||||
// 'independent' => "Independent Mode", 'first person' => 'First Person', etc.
|
||||
var cameraMenuItem = (mode+'')
|
||||
.replace(/^(independent|entity)$/, '$1 mode')
|
||||
.replace(/\b[a-z]/g, function(ch) {
|
||||
return ch.toUpperCase();
|
||||
});
|
||||
|
||||
log('working around Camera.mode bug by enabling the menuItem:', cameraMenuItem);
|
||||
Menu.setIsOptionChecked(cameraMenuItem, true);
|
||||
};
|
||||
}
|
||||
|
||||
function getSystemMetadata() {
|
||||
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system');
|
||||
return {
|
||||
mode: {
|
||||
hmd: HMD.active,
|
||||
desktop: !HMD.active,
|
||||
toolbar: Uuid.isNull(HMD.tabletID),
|
||||
tablet: !Uuid.isNull(HMD.tabletID),
|
||||
},
|
||||
tablet: {
|
||||
toolbarMode: tablet.toolbarMode,
|
||||
desktopScale: Settings.getValue('desktopTabletScale'),
|
||||
hmdScale: Settings.getValue('hmdTabletScale'),
|
||||
},
|
||||
window: {
|
||||
width: Window.innerWidth,
|
||||
height: Window.innerHeight,
|
||||
},
|
||||
desktop: {
|
||||
width: Desktop.width,
|
||||
height: Desktop.height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
main();
|
||||
|
||||
if (typeof module !== 'object') {
|
||||
// if uncaught exceptions occur, show the first in an alert with option to stop the script
|
||||
Script.unhandledException.connect(function onUnhandledException(error) {
|
||||
Script.unhandledException.disconnect(onUnhandledException);
|
||||
log('UNHANDLED EXCEPTION', error, error && error.stack);
|
||||
try {
|
||||
cameraControls.disable();
|
||||
} catch (e) {}
|
||||
// show the error message and first two stack entries
|
||||
var trace = _utils.normalizeStackTrace(error);
|
||||
var message = [ error ].concat(trace.split('\n').slice(0,2)).concat('stop script?').join('\n');
|
||||
Window.confirm('app-camera-move error: ' + message.substr(0,256)) && Script.stop();
|
||||
});
|
||||
}
|
1340
unpublishedScripts/marketplace/camera-move/app.html
Normal file
1340
unpublishedScripts/marketplace/camera-move/app.html
Normal file
File diff suppressed because it is too large
Load diff
613
unpublishedScripts/marketplace/camera-move/app.js
Normal file
613
unpublishedScripts/marketplace/camera-move/app.js
Normal file
|
@ -0,0 +1,613 @@
|
|||
// app.js -- support functions
|
||||
|
||||
/* eslint-env console, jquery, browser, shared-node-browser */
|
||||
/* eslint-disable comma-dangle */
|
||||
/* global Mousetrap, TooltipManager, SettingsJSON, PARAMS, signal, assert, log, debugPrint */
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
var viewportUpdated, bridgedSettings, jquerySettings, tooltipManager, lessManager;
|
||||
|
||||
function setupUI() {
|
||||
$('#debug-menu button, footer button')
|
||||
.hifiButton({
|
||||
create: function() {
|
||||
$(this).addClass('tooltip-target')
|
||||
.data('tooltipster', { side: ['top','bottom'] });
|
||||
}
|
||||
});
|
||||
|
||||
var $json = SettingsJSON;
|
||||
|
||||
window.buttonHandlers = {
|
||||
'test-event-bridge': function() {
|
||||
log('bridgedSettings.eventBridge === Window.EventBridge', bridgedSettings.eventBridge === window.EventBridge);
|
||||
bridgedSettings.sendEvent({ method: 'test-event-bridge' });
|
||||
EventBridge.emitWebEvent('EventBridge.emitWebEvent: testing 1..2..3..');
|
||||
},
|
||||
'page-reload': function() {
|
||||
log('triggering location.reload');
|
||||
location.reload();
|
||||
},
|
||||
'script-reload': function() {
|
||||
log('triggering script.reload');
|
||||
bridgedSettings.sendEvent({ method: 'reloadClientScript' });
|
||||
},
|
||||
'reset-sensors': function() {
|
||||
log('resetting avatar orientation');
|
||||
bridgedSettings.sendEvent({ method: 'resetSensors' });
|
||||
},
|
||||
'reset-to-defaults': function() {
|
||||
tooltipManager && tooltipManager.closeAll();
|
||||
document.activeElement && document.activeElement.blur();
|
||||
document.body.focus();
|
||||
setTimeout(function() {
|
||||
bridgedSettings.sendEvent({ method: 'reset' });
|
||||
},1);
|
||||
},
|
||||
'copy-json': $json.showSettings.bind($json, jquerySettings, null),
|
||||
'paste-json': function() {
|
||||
$json.applyJSON(
|
||||
jquerySettings,
|
||||
'pasted',
|
||||
$json.promptJSON()
|
||||
);
|
||||
},
|
||||
'toggle-advanced-options': function(evt) {
|
||||
var checkbox = $(this).hifiButton('instance').checkbox;
|
||||
var on = checkbox.value(!checkbox.value());
|
||||
$('body').toggleClass('ui-show-advanced-options', on);
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
if ($(this).is('.tooltipstered')) {
|
||||
$(this).tooltipster('instance').content((on ? 'hide' : 'show') + ' advanced options');
|
||||
}
|
||||
if (checkbox.value()) {
|
||||
$('.scrollable').delay(100).animate({
|
||||
scrollTop: innerHeight - $('header').innerHeight() - 24
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
'appVersion': function(evt) {
|
||||
evt.shiftKey && $json.showSettings(jquerySettings);
|
||||
},
|
||||
'errors': function(evt) {
|
||||
$(evt.target).is('button') && $(this).find('.output').text('').end().hide();
|
||||
},
|
||||
};
|
||||
buttonHandlers['button-toggle-advanced-options'] =
|
||||
buttonHandlers['toggle-advanced-options'];
|
||||
Object.keys(window.buttonHandlers).forEach(function(p) {
|
||||
$('#' + p).on('click', window.buttonHandlers[p]);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// trim whitespace in labels
|
||||
$('label').contents().filter(function() {
|
||||
if (this.nodeType !== window.Node.TEXT_NODE) {
|
||||
return false;
|
||||
}
|
||||
this.textContent = this.textContent.trim();
|
||||
return !this.textContent.length;
|
||||
}).remove();
|
||||
|
||||
var settingsNodes = $('fieldset,button[data-for],input:not([type=radio])');
|
||||
settingsNodes.each(function() {
|
||||
// set up the bidirectional mapping between DOM and Settings
|
||||
jquerySettings.registerNode(this);
|
||||
});
|
||||
|
||||
var spinnerOptions = {
|
||||
disabled: true,
|
||||
create: function() {
|
||||
var input = $(this),
|
||||
key = assert(jquerySettings.getKey(input.data('for')));
|
||||
|
||||
var options = input.hifiSpinner('instance').options;
|
||||
options.min = options.min || 0.0;
|
||||
|
||||
bridgedSettings.getValueAsync(key, function(err, result) {
|
||||
input.filter(':not([data-autoenable=false])').hifiSpinner('enable');
|
||||
jquerySettings.setValue(key, result);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
$( ".rows > label" ).each(function() {
|
||||
var label = $(this),
|
||||
input = label.find('input'),
|
||||
type = input.data('type') || input.attr('type');
|
||||
label.wrap('<div>').parent().addClass(['hifi-'+type, type, 'row'].join(' '))
|
||||
.on('focus.row, click.row, hover.row', function() {
|
||||
$(this).find('.tooltipstered').tooltipster('open');
|
||||
});
|
||||
});
|
||||
|
||||
debugPrint('initializing hifiSpinners');
|
||||
// numeric settings
|
||||
$( ".number.row" )
|
||||
.find( "input[data-type=number]" )
|
||||
.addClass('setting')
|
||||
.hifiSpinner(spinnerOptions);
|
||||
|
||||
// radio groups settings
|
||||
$( ".radio.rows" )
|
||||
.find('label').addClass('tooltip-target').end()
|
||||
.addClass('setting')
|
||||
.hifiRadioGroup({
|
||||
direction: 'vertical',
|
||||
disabled: true,
|
||||
create: function() {
|
||||
assert(this !== window);
|
||||
var group = $(this), id = this.id;
|
||||
var key = assert(jquerySettings.getKey(group.data('for')));
|
||||
|
||||
bridgedSettings.getValueAsync(key, function(err, result) {
|
||||
debugPrint('> GOT RADIO', key, id, result);
|
||||
group.filter(':not([data-autoenable=false])').hifiRadioGroup('enable');
|
||||
jquerySettings.setValue(key, result);
|
||||
group.change();
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
// checkbox settings
|
||||
$( "input[type=checkbox]" )
|
||||
.addClass('setting')
|
||||
.hifiCheckbox({
|
||||
disabled: true,
|
||||
create: function() {
|
||||
var key = assert(jquerySettings.getKey(this.id)),
|
||||
input = $(this);
|
||||
input.closest('label').addClass('tooltip-target');
|
||||
bridgedSettings.getValueAsync(key, function(err, result) {
|
||||
input.filter(':not([data-autoenable=false])').hifiCheckbox('enable');
|
||||
jquerySettings.setValue(key, result);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// slider + numeric settings
|
||||
// use the whole row as a tooltip target
|
||||
$( ".slider.row" ).addClass('tooltip-target').data('tooltipster', {
|
||||
distance: -20,
|
||||
side: ['top', 'bottom'],
|
||||
}).each(function(ent) {
|
||||
var element = $(this).find( ".control" ),
|
||||
input = $(this).find('input'),
|
||||
id = input.prop('id'),
|
||||
key = assert(jquerySettings.getKey(id));
|
||||
|
||||
var commonOptions = {
|
||||
disabled: true,
|
||||
min: parseFloat(input.prop('min') || 0),
|
||||
max: parseFloat(input.prop('max') || 10),
|
||||
step: parseFloat(input.prop('step') || 0.01),
|
||||
autoenable: input.data('autoenable') !== 'false',
|
||||
};
|
||||
debugPrint('commonOptions', commonOptions);
|
||||
|
||||
// see: https://api.jqueryui.com/slider/ for more options
|
||||
var slider = element.hifiSlider(Object.assign({
|
||||
orientation: "horizontal",
|
||||
range: "min",
|
||||
animate: 'fast',
|
||||
value: 0.0
|
||||
}, commonOptions)).hifiSlider('instance');
|
||||
|
||||
debugPrint('initializing hifiSlider->hifiSpinner');
|
||||
// setup chrome up/down arrow steps and propagate input field -> slider
|
||||
var spinner = input.on('change', function() {
|
||||
var value = spinner.value();
|
||||
if (isFinite(value) && slider.value() !== value) {
|
||||
slider.value(value);
|
||||
}
|
||||
}).addClass('setting')
|
||||
.hifiSpinner(
|
||||
Object.assign({}, commonOptions, { max: 1e4 })
|
||||
).hifiSpinner('instance');
|
||||
|
||||
bridgedSettings.getValueAsync(key, function(err, result) {
|
||||
slider.options.autoenable !== false && slider.enable();
|
||||
spinner.options.autoenable !== false && spinner.enable();
|
||||
spinner.value(result);
|
||||
});
|
||||
});
|
||||
|
||||
$('#fps').hifiSpinner(spinnerOptions).closest('.row').css('pointer-events', 'all').on('click.subinput', function(evt) {
|
||||
jquerySettings.setValue('thread-update-mode', 'requestAnimationFrame');
|
||||
evt.target.focus();
|
||||
});
|
||||
// detect invalid numbers entered into spinner fields
|
||||
$(':ui-hifiSpinner').on('change.validation', function(evt) {
|
||||
var spinner = $(this).hifiSpinner('instance');
|
||||
$(this).closest('.row').toggleClass('invalid', !spinner.isValid());
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// allow tabbing between checkboxes using the container row
|
||||
$(':ui-hifiCheckbox,:ui-hifiRadioButton').prop('tabindex', -1).closest('.row').prop('tabindex', 0);
|
||||
|
||||
|
||||
// select the input field text when first focused
|
||||
$('input').not('input[type=radio],input[type=checkbox]').on('focus', function () {
|
||||
var dt = (new Date - this.blurredAt);
|
||||
if (!(dt < 5)) { // debounce
|
||||
this.blurredAt = +new Date;
|
||||
$(this).one('mouseup.selectit', function() {
|
||||
$(this).select();
|
||||
return false;
|
||||
}).select();
|
||||
}
|
||||
}).on('blur', function(e) {
|
||||
this.blurredAt = new Date;
|
||||
});
|
||||
|
||||
// monitor changes to specific settings that affect the UI
|
||||
var monitors = {
|
||||
// advanced options toggle
|
||||
'ui-show-advanced-options': function onChange(value) {
|
||||
function handle(err, result) {
|
||||
log('** ui-show-advanced-options updated', result+'');
|
||||
$('body').toggleClass('ui-show-advanced-options', !!result);
|
||||
jquerySettings.setValue('ui-show-advanced-options', result)
|
||||
}
|
||||
if (!onChange.fetched) {
|
||||
bridgedSettings.getValueAsync('ui-show-advanced-options', handle);
|
||||
return onChange.fetched = true;
|
||||
}
|
||||
handle(null, value);
|
||||
},
|
||||
|
||||
// UI tooltips toggle
|
||||
'ui-enable-tooltips': function(value) {
|
||||
if (!tooltipManager) return;
|
||||
if (value) {
|
||||
tooltipManager.enable();
|
||||
tooltipManager.openFocusedTooltip();
|
||||
} else {
|
||||
tooltipManager.disable();
|
||||
}
|
||||
},
|
||||
|
||||
// enable/disable fps field (based on whether thread mode is requestAnimationFrame)
|
||||
'thread-update-mode': function(value) {
|
||||
var enabled = (value === 'requestAnimationFrame'), fps = $('#fps');
|
||||
fps.hifiSpinner(enabled ? 'enable' : 'disable');
|
||||
fps.closest('.row').toggleClass('disabled', !enabled);
|
||||
},
|
||||
|
||||
// flag BODY with CSS class to indicate active camera move mode
|
||||
'camera-move-enabled': function(value) {
|
||||
$('body').toggleClass('camera-move-enabled', value);
|
||||
},
|
||||
|
||||
// update the "keybinding" and #appVersion extraParams displays
|
||||
'.extraParams': function extraParams(value, other) {
|
||||
value = bridgedSettings.extraParams;
|
||||
if (value.mode) {
|
||||
for (var p in value.mode) {
|
||||
// tablet-mode, hmd-mode, etc.
|
||||
$('body').toggleClass(p + '-mode', value.mode[p]);
|
||||
}
|
||||
document.oncontextmenu = value.mode.tablet ? function(evt) { return evt.preventDefault(),false; } : null;
|
||||
$('[data-type=number]').prop('type', value.mode.tablet ? 'number' : 'text');
|
||||
}
|
||||
var versionDisplay = [
|
||||
value.appVersion || '(unknown appVersion)',
|
||||
PARAMS.debug && '(debug)',
|
||||
value.mode && value.mode.tablet ? '(tablet)' : '',
|
||||
].filter(Boolean).join(' | ');
|
||||
$('#appVersion').find('.output').text(versionDisplay).end().show();
|
||||
|
||||
if (value.toggleKey) {
|
||||
$('#toggleKey').find('.binding').empty()
|
||||
.append(getKeysHTML(value.toggleKey)).end().show();
|
||||
}
|
||||
|
||||
var activateLookAtOption = value.MyAvatar && value.MyAvatar.supportsLookAtSnappingEnabled;
|
||||
$(jquerySettings.findNodeByKey('Avatar/lookAtSnappingEnabled'))
|
||||
.hifiCheckbox(activateLookAtOption ? 'enable' : 'disable')
|
||||
.closest('.row').toggleClass('disabled', !activateLookAtOption)
|
||||
.css('pointer-events', 'all') // so tooltips display regardless
|
||||
|
||||
var activateCursorOption = value.Reticle && value.Reticle.supportsScale;
|
||||
$('#minimal-cursor:ui-hifiCheckbox')
|
||||
.hifiCheckbox(activateCursorOption ? 'enable' : 'disable')
|
||||
.closest('.row').toggleClass('disabled', !activateCursorOption)
|
||||
.css('pointer-events', 'all') // so tooltips display regardless
|
||||
},
|
||||
|
||||
// gray out / ungray out page content if user is mouse looking around in Interface
|
||||
// (otherwise the cursor still interacts with web content...)
|
||||
'Keyboard.RightMouseButton': function(localValue, key, value) {
|
||||
debugPrint(localValue, '... Keyboard.RightMouseButton:' + value);
|
||||
window.active = !value;
|
||||
},
|
||||
};
|
||||
monitors['toggle-advanced-options'] = monitors['ui-toggle-advanced-options'];
|
||||
monitorSettings(monitors);
|
||||
// disable selection
|
||||
// $('input').css('-webkit-user-select', 'none');
|
||||
|
||||
viewportUpdated.connect(lessManager, 'onViewportUpdated');
|
||||
|
||||
setupTooltips();
|
||||
|
||||
$(window).trigger('resize'); // populate viewport
|
||||
|
||||
// set up DOM MutationObservers
|
||||
settingsNodes.each(function tcobo() {
|
||||
if (this.dataset.hifiType === 'hifiButton') {
|
||||
return;
|
||||
}
|
||||
var id = assert(this.dataset['for'] || this.id, 'could not id for node: ' + this.outerHTML);
|
||||
assert(!tcobo[id]); // detect dupes
|
||||
tcobo[id] = true;
|
||||
debugPrint('OBSERVING NODE', id, this.id || this.getAttribute('for'));
|
||||
jquerySettings.observeNode(this);
|
||||
});
|
||||
|
||||
// set up key bindings
|
||||
setupMousetrapKeys();
|
||||
|
||||
function getKeysHTML(binding) {
|
||||
var text = binding.text || ('(#' + binding.key + ')');
|
||||
// translate hifi's proprietary key scheme into human-friendly KBDs
|
||||
return [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ]
|
||||
.map(function(flag) {
|
||||
return binding['is' + flag] && flag;
|
||||
})
|
||||
.concat(text)
|
||||
.filter(Boolean)
|
||||
.map(function(key) {
|
||||
return '<kbd>' + key.replace('Shifted','Shift') + '</kbd>';
|
||||
})
|
||||
.join('-');
|
||||
}
|
||||
} // setupUI
|
||||
|
||||
function setupTooltips() {
|
||||
// extract the tooltip captions
|
||||
var tooltips = window.tooltips = {};
|
||||
var target = '[id], [data-for], [for]';
|
||||
$('.tooltip')
|
||||
.removeClass('tooltip').addClass('x-tooltip')
|
||||
.each(function() {
|
||||
var element = $(this),
|
||||
input = $(element.parent().find('input').get(0) ||
|
||||
element.closest('button').get(0));
|
||||
id = element.prop('id') || element.data('for') ||
|
||||
input.prop('id') || input.data('for');
|
||||
assert(id);
|
||||
tooltips[id] = this.outerHTML;
|
||||
}).hide();
|
||||
tooltipManager = new TooltipManager({
|
||||
enabled: false,
|
||||
testmode: PARAMS.tooltiptest,
|
||||
viewport: PARAMS.viewport,
|
||||
tooltips: tooltips,
|
||||
elements: '#reset-to-defaults, button, input',
|
||||
});
|
||||
viewportUpdated.connect(tooltipManager, 'updateViewport');
|
||||
// tooltips aren't needed right away, so defer initializing for better page load times
|
||||
window.setTimeout(tooltipManager.initialize.bind(tooltipManager), 1000);
|
||||
}
|
||||
|
||||
// helper for instrumenting local jquery onchange handlers
|
||||
function monitorSettings(options) {
|
||||
return Object.keys(options).reduce(function(out, id) {
|
||||
var key = bridgedSettings.resolve(id),
|
||||
domId = jquerySettings.getId(key, true);
|
||||
|
||||
if (!domId) {
|
||||
var placeholder = {
|
||||
id: id,
|
||||
type: 'placeholder',
|
||||
toString: function() {
|
||||
return this.id;
|
||||
},
|
||||
value: undefined,
|
||||
};
|
||||
jquerySettings.registerSetting(placeholder, key);
|
||||
debugPrint('registered placeholder value for setting', id, key);
|
||||
assert(jquerySettings.findNodeByKey(key) === placeholder);
|
||||
}
|
||||
|
||||
// if (domId === 'toggle-advanced-options') alert([key,id,domId, jquerySettings.findNodeByKey(key)])
|
||||
assert(function assertion(){
|
||||
return typeof key === 'string';
|
||||
}, 'monitorSettings -- received invalid key type');
|
||||
|
||||
var context = {
|
||||
id: id,
|
||||
key: key,
|
||||
domId: domId,
|
||||
options: options,
|
||||
lastValue: undefined,
|
||||
initializer: function(hint) {
|
||||
var key = this.key,
|
||||
lastValue = this.lastValue;
|
||||
if (lastValue !== undefined) {
|
||||
return log('skipping repeat initializer', key, hint);
|
||||
}
|
||||
this.lastValue = lastValue = jquerySettings.getValue(key);
|
||||
this._onChange.call(jquerySettings, key, lastValue, undefined, hint);
|
||||
},
|
||||
_onChange: function _onChange(key, value) {
|
||||
var currentValue = this.getValue(context.id),
|
||||
jsonCurrentValue = JSON.stringify(currentValue);
|
||||
|
||||
if (jsonCurrentValue === context.jsonLastValue) {
|
||||
if (jsonCurrentValue !== undefined) {
|
||||
debugPrint([context.key, '_onChange', this, 'not triggering _onChange for duplicated value']);
|
||||
}
|
||||
return;
|
||||
}
|
||||
context.jsonLastValue = jsonCurrentValue;
|
||||
var args = [].slice.call(arguments, 0);
|
||||
debugPrint('monitorSetting._onChange', context.key, value, [].concat(args).pop());
|
||||
context.options[context.id].apply(this, [ currentValue ].concat(args));
|
||||
},
|
||||
|
||||
onValueReceived: function(key) {
|
||||
if (key === this.key) {
|
||||
this._onChange.apply(bridgedSettings, arguments);
|
||||
}
|
||||
},
|
||||
onMutationEvent: function(event) {
|
||||
if (event.key === this.key) {
|
||||
context._onChange.call(jquerySettings, event.key, event.value, event.oldValue, event.hifiType+':mutation');
|
||||
}
|
||||
},
|
||||
onPendingRequestsFinished: function onPendingRequestsFinished() {
|
||||
bridgedSettings.pendingRequestsFinished.disconnect(this, 'onPendingRequestsFinished');
|
||||
this.initializer('pendingRequestsFinished');
|
||||
},
|
||||
};
|
||||
|
||||
bridgedSettings.valueReceived.connect(context, 'onValueReceived');
|
||||
jquerySettings.mutationEvent.connect(context, 'onMutationEvent');
|
||||
|
||||
if (bridgedSettings.pendingRequestCount()) {
|
||||
bridgedSettings.pendingRequestsFinished.connect(context, 'onPendingRequestsFinished');
|
||||
} else {
|
||||
window.setTimeout(context.initializer.bind(context, 'monitorSettings init'), 1);
|
||||
}
|
||||
return context;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function initializeDOM() {
|
||||
|
||||
Object.defineProperty(window, 'active', {
|
||||
get: function() {
|
||||
return window._active;
|
||||
},
|
||||
set: function(nv) {
|
||||
nv = !!nv;
|
||||
window._active = nv;
|
||||
debugPrint('window.active == ' + nv);
|
||||
if (!nv) {
|
||||
document.activeElement && document.activeElement.blur();
|
||||
document.body.focus();
|
||||
tooltipManager && tooltipManager.disable();
|
||||
debugPrint('TOOLTIPS DISABLED');
|
||||
} else if (tooltipManager && bridgedSettings) {
|
||||
if (bridgedSettings.getValue('ui-enable-tooltips')){
|
||||
tooltipManager.enable();
|
||||
debugPrint('TOOLTIPS RE-ENABLED');
|
||||
}
|
||||
}
|
||||
$('body').toggleClass('active', window._active);
|
||||
},
|
||||
});
|
||||
$('body').toggleClass('active', window._active = true);
|
||||
|
||||
function checkAnim(evt) {
|
||||
if (!checkAnim.disabled) {
|
||||
if ($('.scrollable').is(':animated')) {
|
||||
$('.scrollable').stop();
|
||||
log(evt.type, 'stop animation');
|
||||
}
|
||||
}
|
||||
}
|
||||
viewportUpdated = signal(function viewportUpdated(viewport) {});
|
||||
function triggerViewportUpdate() {
|
||||
var viewport = {
|
||||
inner: { width: innerWidth, height: innerHeight },
|
||||
client: {
|
||||
width: document.body.clientWidth || window.innerWidth,
|
||||
height: document.body.clientHeight || window.innerHeight,
|
||||
},
|
||||
min: { width: window.innerWidth / 3, height: 32 },
|
||||
max: { width: window.innerWidth * 7/8, height: window.innerHeight * 7/8 },
|
||||
};
|
||||
debugPrint('viewportUpdated', viewport);
|
||||
PARAMS.viewport = Object.assign(PARAMS.viewport||{}, viewport);
|
||||
viewportUpdated(viewport, triggerViewportUpdate.lastViewport);
|
||||
triggerViewportUpdate.lastViewport = viewport;
|
||||
}
|
||||
$(window).on({
|
||||
resize: function resize() {
|
||||
window.clearTimeout(resize.to);
|
||||
resize.to = window.setTimeout(triggerViewportUpdate, 100);
|
||||
},
|
||||
mousedown: checkAnim, mouseup: checkAnim, scroll: checkAnim, wheel: checkAnim,
|
||||
blur: function() {
|
||||
log('** BLUR ** ');
|
||||
$('body').addClass('window-blurred');
|
||||
document.body.focus();
|
||||
document.activeElement && document.activeElement.blur();
|
||||
// tooltipManager.closeAll();
|
||||
},
|
||||
focus: function() {
|
||||
log('** FOCUS **');
|
||||
$('body').removeClass('window-blurred');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setupMousetrapKeys() {
|
||||
if (!window.Mousetrap) {
|
||||
return log('WARNING: window.Mousetrap not found; not configurating keybindings');
|
||||
}
|
||||
mousetrapMultiBind({
|
||||
'ctrl+a, option+a': function global(evt, combo) {
|
||||
$(document.activeElement).filter('input').select();
|
||||
},
|
||||
'enter': function global(evt, combo) {
|
||||
var node = document.activeElement;
|
||||
if ($(node).is('input')) {
|
||||
log('enter on input element');
|
||||
tooltipManager.closeAll();
|
||||
node.blur();
|
||||
var nexts = $('[tabindex],input,:focusable').not('[tabindex=-1],.ui-slider-handle');
|
||||
nexts.add(nexts.find('input'));
|
||||
nexts = nexts.toArray();
|
||||
if (~nexts.indexOf(node)) {
|
||||
var nextActive = nexts[nexts.indexOf(node)+1];
|
||||
log('setting focus to', nextActive);
|
||||
$(nextActive).focus();
|
||||
} else {
|
||||
log('could not deduce next tabbable element', nexts.length, this);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
'ctrl+w': bridgedSettings.sendEvent.bind(bridgedSettings, { method: 'window.close' }),
|
||||
'r': location.reload.bind(location),
|
||||
'space': function global(evt, combo) {
|
||||
log('SPACE', evt.target, document.activeElement);
|
||||
$(document.activeElement).filter('.row').find(':ui-hifiCheckbox,:ui-hifiRadioButton').click();
|
||||
if (!$(document.activeElement).is('input,.ui-widget')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// $('input').addClass('mousetrap');
|
||||
function mousetrapMultiBind(a, b) {
|
||||
var obj = typeof a === 'object' ? a :
|
||||
Object.defineProperty({}, a, {enumerable: true, value: b });
|
||||
Object.keys(obj).forEach(function(key) {
|
||||
var method = obj[key].name === 'global' ? 'bindGlobal' : 'bind';
|
||||
key.split(/\s*,\s*/).forEach(function(combo) {
|
||||
debugPrint('Mousetrap', method, combo, typeof obj[key]);
|
||||
Mousetrap[method](combo, function(evt, combo) {
|
||||
debugPrint('Mousetrap', method, combo);
|
||||
return obj[key].apply(this, arguments);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// support the URL having a #node-id (or #debug=1&node-id) hash fragment jumping to that element
|
||||
function jumpToAnchor(id) {
|
||||
id = JSON.stringify(id);
|
||||
$('[id='+id+'],[name='+id+']').first().each(function() {
|
||||
log('jumpToAnchor', id);
|
||||
$(this).show();
|
||||
this.scrollIntoView({ behavior: 'smooth' });
|
||||
});
|
||||
};
|
268
unpublishedScripts/marketplace/camera-move/avatar-updater.js
Normal file
268
unpublishedScripts/marketplace/camera-move/avatar-updater.js
Normal file
|
@ -0,0 +1,268 @@
|
|||
/* eslint-env commonjs */
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// helper module that performs the avatar movement update calculations
|
||||
|
||||
module.exports = AvatarUpdater;
|
||||
|
||||
var _utils = require('./modules/_utils.js'),
|
||||
assert = _utils.assert;
|
||||
|
||||
var movementUtils = require('./modules/movement-utils.js');
|
||||
|
||||
function AvatarUpdater(options) {
|
||||
options = options || {};
|
||||
assert(function assertion() {
|
||||
return typeof options.getCameraMovementSettings === 'function' &&
|
||||
typeof options.getMovementState === 'function' &&
|
||||
options.globalState;
|
||||
});
|
||||
|
||||
var DEFAULT_MOTOR_TIMESCALE = 1e6; // a large value that matches Interface's default
|
||||
var EASED_MOTOR_TIMESCALE = 0.01; // a small value to make Interface quickly apply MyAvatar.motorVelocity
|
||||
var EASED_MOTOR_THRESHOLD = 0.1; // above this speed (m/s) EASED_MOTOR_TIMESCALE is used
|
||||
var ACCELERATION_MULTIPLIERS = { translation: 1, rotation: 1, zoom: 1 };
|
||||
var STAYGROUNDED_PITCH_THRESHOLD = 45.0; // degrees; ground level is maintained when pitch is within this threshold
|
||||
var MIN_DELTA_TIME = 0.0001; // to avoid math overflow, never consider dt less than this value
|
||||
var DEG_TO_RAD = Math.PI / 180.0;
|
||||
update.frameCount = 0;
|
||||
update.endTime = update.windowStartTime = _utils.getRuntimeSeconds();
|
||||
update.windowFrame = update.windowStartFrame= 0;
|
||||
|
||||
this.update = update;
|
||||
this.options = options;
|
||||
this._resetMyAvatarMotor = _resetMyAvatarMotor;
|
||||
this._applyDirectPitchYaw = _applyDirectPitchYaw;
|
||||
|
||||
var globalState = options.globalState;
|
||||
var getCameraMovementSettings = options.getCameraMovementSettings;
|
||||
var getMovementState = options.getMovementState;
|
||||
var _debugChannel = options.debugChannel;
|
||||
function update(dt) {
|
||||
update.frameCount++;
|
||||
var startTime = _utils.getRuntimeSeconds();
|
||||
var settings = getCameraMovementSettings(),
|
||||
EPSILON = settings.epsilon;
|
||||
|
||||
var independentCamera = Camera.mode === 'independent',
|
||||
headPitch = MyAvatar.headPitch;
|
||||
|
||||
var actualDeltaTime = startTime - update.endTime,
|
||||
practicalDeltaTime = Math.max(MIN_DELTA_TIME, actualDeltaTime),
|
||||
deltaTime;
|
||||
|
||||
if (settings.useConstantDeltaTime) {
|
||||
deltaTime = settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ?
|
||||
(1 / settings.fps) : (1 / 90);
|
||||
} else if (settings.threadMode === movementUtils.CameraControls.SCRIPT_UPDATE) {
|
||||
deltaTime = dt;
|
||||
} else {
|
||||
deltaTime = practicalDeltaTime;
|
||||
}
|
||||
|
||||
var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation',
|
||||
currentOrientation = independentCamera ? Camera.orientation : MyAvatar[orientationProperty],
|
||||
currentPosition = MyAvatar.position;
|
||||
|
||||
var previousValues = globalState.previousValues,
|
||||
pendingChanges = globalState.pendingChanges,
|
||||
currentVelocities = globalState.currentVelocities;
|
||||
|
||||
var movementState = getMovementState({ update: deltaTime }),
|
||||
targetState = movementUtils.applyEasing(deltaTime, 'easeIn', settings, movementState, ACCELERATION_MULTIPLIERS),
|
||||
dragState = movementUtils.applyEasing(deltaTime, 'easeOut', settings, currentVelocities, ACCELERATION_MULTIPLIERS);
|
||||
|
||||
currentVelocities.integrate(targetState, currentVelocities, dragState, settings);
|
||||
|
||||
var currentSpeed = Vec3.length(currentVelocities.translation),
|
||||
targetSpeed = Vec3.length(movementState.translation),
|
||||
verticalHold = movementState.isGrounded && settings.stayGrounded && Math.abs(headPitch) < STAYGROUNDED_PITCH_THRESHOLD;
|
||||
|
||||
var deltaOrientation = Quat.fromVec3Degrees(Vec3.multiply(deltaTime, currentVelocities.rotation)),
|
||||
targetOrientation = Quat.normalize(Quat.multiply(currentOrientation, deltaOrientation));
|
||||
|
||||
var targetVelocity = Vec3.multiplyQbyV(targetOrientation, currentVelocities.translation);
|
||||
|
||||
if (verticalHold) {
|
||||
targetVelocity.y = 0;
|
||||
}
|
||||
|
||||
var deltaPosition = Vec3.multiply(deltaTime, targetVelocity);
|
||||
|
||||
_resetMyAvatarMotor(pendingChanges);
|
||||
|
||||
if (!independentCamera) {
|
||||
var DriveModes = movementUtils.DriveModes;
|
||||
switch (settings.driveMode) {
|
||||
case DriveModes.MOTOR: {
|
||||
if (currentSpeed > EPSILON || targetSpeed > EPSILON) {
|
||||
var motorTimescale = (currentSpeed > EASED_MOTOR_THRESHOLD ? EASED_MOTOR_TIMESCALE : DEFAULT_MOTOR_TIMESCALE);
|
||||
var motorPitch = Quat.fromPitchYawRollDegrees(headPitch, 180, 0),
|
||||
motorVelocity = Vec3.multiplyQbyV(motorPitch, currentVelocities.translation);
|
||||
if (verticalHold) {
|
||||
motorVelocity.y = 0;
|
||||
}
|
||||
Object.assign(pendingChanges.MyAvatar, {
|
||||
motorVelocity: motorVelocity,
|
||||
motorTimescale: motorTimescale
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DriveModes.THRUST: {
|
||||
var thrustVector = currentVelocities.translation,
|
||||
maxThrust = settings.translation.maxVelocity,
|
||||
thrust;
|
||||
if (targetSpeed > EPSILON) {
|
||||
thrust = movementUtils.calculateThrust(maxThrust * 5, thrustVector, previousValues.thrust);
|
||||
} else if (currentSpeed > 1 && Vec3.length(previousValues.thrust) > 1) {
|
||||
thrust = Vec3.multiply(-currentSpeed / 10.0, thrustVector);
|
||||
} else {
|
||||
thrust = Vec3.ZERO;
|
||||
}
|
||||
if (thrust) {
|
||||
thrust = Vec3.multiplyQbyV(MyAvatar[orientationProperty], thrust);
|
||||
if (verticalHold) {
|
||||
thrust.y = 0;
|
||||
}
|
||||
}
|
||||
previousValues.thrust = pendingChanges.MyAvatar.setThrust = thrust;
|
||||
break;
|
||||
}
|
||||
case DriveModes.POSITION: {
|
||||
pendingChanges.MyAvatar.position = Vec3.sum(currentPosition, deltaPosition);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown driveMode: ' + settings.driveMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var finalOrientation;
|
||||
switch (Camera.mode) {
|
||||
case 'mirror': // fall through
|
||||
case 'independent':
|
||||
targetOrientation = settings.preventRoll ? Quat.cancelOutRoll(targetOrientation) : targetOrientation;
|
||||
var boomVector = Vec3.multiply(-currentVelocities.zoom.z, Quat.getFront(targetOrientation)),
|
||||
deltaCameraPosition = Vec3.sum(boomVector, deltaPosition);
|
||||
Object.assign(pendingChanges.Camera, {
|
||||
position: Vec3.sum(Camera.position, deltaCameraPosition),
|
||||
orientation: targetOrientation
|
||||
});
|
||||
break;
|
||||
case 'entity':
|
||||
finalOrientation = targetOrientation;
|
||||
break;
|
||||
default: // 'first person', 'third person'
|
||||
finalOrientation = targetOrientation;
|
||||
break;
|
||||
}
|
||||
|
||||
if (settings.jitterTest) {
|
||||
finalOrientation = Quat.multiply(MyAvatar[orientationProperty], Quat.fromPitchYawRollDegrees(0, 60 * deltaTime, 0));
|
||||
// Quat.fromPitchYawRollDegrees(0, _utils.getRuntimeSeconds() * 60, 0)
|
||||
}
|
||||
|
||||
if (finalOrientation) {
|
||||
if (settings.preventRoll) {
|
||||
finalOrientation = Quat.cancelOutRoll(finalOrientation);
|
||||
}
|
||||
previousValues.finalOrientation = pendingChanges.MyAvatar[orientationProperty] = Quat.normalize(finalOrientation);
|
||||
}
|
||||
|
||||
if (!movementState.mouseSmooth && movementState.isRightMouseButton) {
|
||||
// directly apply mouse pitch and yaw when mouse smoothing is disabled
|
||||
_applyDirectPitchYaw(deltaTime, movementState, settings);
|
||||
}
|
||||
|
||||
var endTime = _utils.getRuntimeSeconds();
|
||||
var cycleTime = endTime - update.endTime;
|
||||
update.endTime = endTime;
|
||||
|
||||
pendingChanges.submit();
|
||||
|
||||
if ((endTime - update.windowStartTime) > 3) {
|
||||
update.momentaryFPS = (update.frameCount - update.windowStartFrame) /
|
||||
(endTime - update.windowStartTime);
|
||||
update.windowStartFrame = update.frameCount;
|
||||
update.windowStartTime = endTime;
|
||||
}
|
||||
|
||||
if (_debugChannel && update.windowStartFrame === update.frameCount) {
|
||||
Messages.sendLocalMessage(_debugChannel, JSON.stringify({
|
||||
threadFrames: update.threadFrames,
|
||||
frame: update.frameCount,
|
||||
threadMode: settings.threadMode,
|
||||
driveMode: settings.driveMode,
|
||||
orientationProperty: orientationProperty,
|
||||
isGrounded: movementState.isGrounded,
|
||||
targetAnimationFPS: settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? settings.fps : undefined,
|
||||
actualFPS: 1 / actualDeltaTime,
|
||||
effectiveAnimationFPS: 1 / deltaTime,
|
||||
seconds: {
|
||||
startTime: startTime,
|
||||
endTime: endTime
|
||||
},
|
||||
milliseconds: {
|
||||
actualDeltaTime: actualDeltaTime * 1000,
|
||||
deltaTime: deltaTime * 1000,
|
||||
cycleTime: cycleTime * 1000,
|
||||
calculationTime: (endTime - startTime) * 1000
|
||||
},
|
||||
finalOrientation: finalOrientation,
|
||||
thrust: thrust,
|
||||
maxVelocity: settings.translation,
|
||||
targetVelocity: targetVelocity,
|
||||
currentSpeed: currentSpeed,
|
||||
targetSpeed: targetSpeed
|
||||
}, 0, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function _resetMyAvatarMotor(targetObject) {
|
||||
if (MyAvatar.motorTimescale !== DEFAULT_MOTOR_TIMESCALE) {
|
||||
targetObject.MyAvatar.motorTimescale = DEFAULT_MOTOR_TIMESCALE;
|
||||
}
|
||||
if (MyAvatar.motorReferenceFrame !== 'avatar') {
|
||||
targetObject.MyAvatar.motorReferenceFrame = 'avatar';
|
||||
}
|
||||
if (Vec3.length(MyAvatar.motorVelocity)) {
|
||||
targetObject.MyAvatar.motorVelocity = Vec3.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
function _applyDirectPitchYaw(deltaTime, movementState, settings) {
|
||||
var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation',
|
||||
rotation = movementState.rotation,
|
||||
speed = Vec3.multiply(-DEG_TO_RAD / 2.0, settings.rotation.speed);
|
||||
|
||||
var previousValues = globalState.previousValues,
|
||||
pendingChanges = globalState.pendingChanges,
|
||||
currentVelocities = globalState.currentVelocities;
|
||||
|
||||
var previous = previousValues.pitchYawRoll,
|
||||
target = Vec3.multiply(deltaTime, Vec3.multiplyVbyV(rotation, speed)),
|
||||
pitchYawRoll = Vec3.mix(previous, target, 0.5),
|
||||
orientation = Quat.fromVec3Degrees(pitchYawRoll);
|
||||
|
||||
previousValues.pitchYawRoll = pitchYawRoll;
|
||||
|
||||
if (pendingChanges.MyAvatar.headOrientation || pendingChanges.MyAvatar.orientation) {
|
||||
var newOrientation = Quat.multiply(MyAvatar[orientationProperty], orientation);
|
||||
delete pendingChanges.MyAvatar.headOrientation;
|
||||
delete pendingChanges.MyAvatar.orientation;
|
||||
if (settings.preventRoll) {
|
||||
newOrientation = Quat.cancelOutRoll(newOrientation);
|
||||
}
|
||||
MyAvatar[orientationProperty] = newOrientation;
|
||||
} else if (pendingChanges.Camera.orientation) {
|
||||
var cameraOrientation = Quat.multiply(Camera.orientation, orientation);
|
||||
if (settings.preventRoll) {
|
||||
cameraOrientation = Quat.cancelOutRoll(cameraOrientation);
|
||||
}
|
||||
Camera.orientation = cameraOrientation;
|
||||
}
|
||||
currentVelocities.rotation = Vec3.ZERO;
|
||||
}
|
||||
}
|
311
unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js
Normal file
311
unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js
Normal file
|
@ -0,0 +1,311 @@
|
|||
// extended jQuery UI controls
|
||||
|
||||
/* eslint-env console, jquery, browser */
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
/* global assert, log, debugPrint */
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// WIDGET BASE
|
||||
Object.assign($.Widget.prototype, {
|
||||
// common bootstrapping across widget types
|
||||
initHifiControl: function initHifiControl(hifiType) {
|
||||
initHifiControl.widgetCount = (initHifiControl.widgetCount || 0) + 1;
|
||||
hifiType = hifiType || this.widgetName;
|
||||
|
||||
var element = this.element, options = this.options, node = element.get(0), dataset = node.dataset;
|
||||
assert(!this.element.is('.initialized'));
|
||||
this.element.addClass('initialized');
|
||||
var attributes = [].reduce.call(node.attributes, function(out, attribute) {
|
||||
out[attribute.name] = attribute.value;
|
||||
return out;
|
||||
}, {});
|
||||
|
||||
var searchOrder = [ options, dataset, attributes, node ];
|
||||
function setData(key, fallback) {
|
||||
var value = searchOrder.map(function(obj) {
|
||||
return obj[key];
|
||||
}).concat(fallback).filter(function(value) {
|
||||
return value !== undefined;
|
||||
})[0];
|
||||
return value === undefined ? null : (dataset[key] = value);
|
||||
}
|
||||
options.hifiWidgetId = hifiType + '-' + initHifiControl.widgetCount;
|
||||
node.id = node.id || options.hifiWidgetId;
|
||||
dataset.hifiType = hifiType;
|
||||
setData('type');
|
||||
setData('for', node.id);
|
||||
setData('checked');
|
||||
if (setData('value', null) !== null) {
|
||||
element.attr('value', dataset.value);
|
||||
}
|
||||
|
||||
return node.id;
|
||||
},
|
||||
hifiFindWidget: function(hifiType, quiet) {
|
||||
var selector = ':ui-'+hifiType;
|
||||
var _for = JSON.stringify(this.element.data('for')||undefined),
|
||||
element = _for && $('[id='+_for+']').filter(selector);
|
||||
if (!element.is(selector)) {
|
||||
element = this.element.closest(selector);
|
||||
}
|
||||
var instance = element.filter(selector)[hifiType]('instance');
|
||||
|
||||
if (!instance && !quiet) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error([
|
||||
instance, 'could not find target instance ' + selector +
|
||||
' for ' + this.element.data('hifi-type') +
|
||||
' #' + this.element.prop('id') + ' for=' + this.element.data('for')
|
||||
]);
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
});
|
||||
|
||||
// CHECKBOX
|
||||
$.widget('ui.hifiCheckbox', $.ui.checkboxradio, {
|
||||
value: function value(nv) {
|
||||
if (arguments.length) {
|
||||
var currentValue = this.element.prop('checked');
|
||||
if (nv !== currentValue){
|
||||
this.element.prop('checked', nv);
|
||||
this.element.change();
|
||||
}
|
||||
}
|
||||
return this.element.prop('checked');
|
||||
},
|
||||
_create: function() {
|
||||
var id = this.initHifiControl();
|
||||
this.element.attr('value', id);
|
||||
// add an implicit label if missing
|
||||
var forId = 'for=' + JSON.stringify(id);
|
||||
var label = $(this.element.get(0)).closest('label').add($('label[' + forId + ']'));
|
||||
if (!label.get(0)) {
|
||||
$('<label ' + forId + '>' + forId + '</label>').appendTo(this.element);
|
||||
}
|
||||
this._super();
|
||||
this.element.on('change._hifiCheckbox, click._hifiCheckbox', function() {
|
||||
var checked = this.value(),
|
||||
attr = this.element.attr('checked');
|
||||
if (checked && !attr) {
|
||||
this.element.attr('checked', 'checked');
|
||||
} else if (!checked && attr) {
|
||||
this.element.removeAttr('checked');
|
||||
}
|
||||
this.refresh();
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
|
||||
// BUTTON
|
||||
$.widget('ui.hifiButton', $.ui.button, {
|
||||
value: function(nv) {
|
||||
var dataset = this.element[0].dataset;
|
||||
if (arguments.length) {
|
||||
var checked = (dataset.checked === 'true');
|
||||
nv = (nv === 'true' || !!nv);
|
||||
if (nv !== checked) {
|
||||
debugPrint('hifibutton checked changed', nv, checked);
|
||||
dataset.checked = nv;
|
||||
this.element.change();
|
||||
} else {
|
||||
debugPrint('hifibutton value same', nv, checked);
|
||||
}
|
||||
}
|
||||
return dataset.checked === 'true';
|
||||
},
|
||||
_create: function() {
|
||||
this.element.data('type', 'checkbox');
|
||||
this.initHifiControl();
|
||||
this._super();
|
||||
this.element[0].dataset.checked = !!this.element.attr('checked');
|
||||
var _for = this.element.data('for') || undefined;
|
||||
if (_for && _for !== this.element[0].id) {
|
||||
_for = JSON.stringify(_for);
|
||||
var checkbox = this.hifiFindWidget('hifiCheckbox', true);
|
||||
if (!checkbox) {
|
||||
var input = $('<label><input type=checkbox id=' + _for + ' value=' + _for +' /></label>').hide();
|
||||
input.appendTo(this.element);
|
||||
checkbox = input.find('input')
|
||||
.hifiCheckbox()
|
||||
.hifiCheckbox('instance');
|
||||
}
|
||||
this.element.find('.tooltip-target').removeClass('tooltip-target');
|
||||
this.element.prop('id', 'button-'+this.element.prop('id'));
|
||||
checkbox.element.on('change._hifiButton', function() {
|
||||
debugPrint('checkbox -> button');
|
||||
this.value(checkbox.value());
|
||||
}.bind(this));
|
||||
this.element.on('change', function() {
|
||||
debugPrint('button -> checkbox');
|
||||
checkbox.value(this.value());
|
||||
}.bind(this));
|
||||
this.checkbox = checkbox;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// RADIO BUTTON
|
||||
$.widget('ui.hifiRadioButton', $.ui.checkboxradio, {
|
||||
value: function value(nv) {
|
||||
if (arguments.length) {
|
||||
this.element.prop('checked', !!nv);
|
||||
this.element.change();
|
||||
}
|
||||
return this.element.prop('checked');
|
||||
},
|
||||
_create: function() {
|
||||
var id = this.initHifiControl();
|
||||
this.element.attr('value', this.element.data('value') || id);
|
||||
// console.log(this.element[0]);
|
||||
assert(this.element.data('for'));
|
||||
this._super();
|
||||
|
||||
this.element.on('change._hifiRadioButton, click._hifiRadioButton', function() {
|
||||
var group = this.hifiFindWidget('hifiRadioGroup'),
|
||||
checked = !!this.element.attr('checked'),
|
||||
dotchecked = this.element.prop('checked'),
|
||||
value = this.element.attr('value');
|
||||
|
||||
if (dotchecked !== checked || group.value() !== value) {
|
||||
if (dotchecked && group.value() !== value) {
|
||||
log(value, 'UPDATING GRUOP', group.element[0].id);
|
||||
group.value(value);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
|
||||
// RADIO GROUP
|
||||
$.widget('ui.hifiRadioGroup', $.ui.controlgroup, {
|
||||
radio: function(selector) {
|
||||
return this.element.find(':ui-hifiRadioButton' + selector).hifiRadioButton('instance');
|
||||
},
|
||||
refresh: function() {
|
||||
var value = this.value();
|
||||
this.element.find(':ui-hifiRadioButton').each(function() {
|
||||
$(this).prop('checked', $(this).attr('value') === value).hifiRadioButton('refresh');
|
||||
});
|
||||
this._super();
|
||||
},
|
||||
value: function value(nv) {
|
||||
if (arguments.length) {
|
||||
var id = this.element[0].id,
|
||||
previous = this.value();
|
||||
debugPrint('RADIOBUTTON GROUP value', id + ' = ' + nv + '(was: ' + previous + ')');
|
||||
this.element.attr('value', nv);
|
||||
this.refresh();
|
||||
}
|
||||
return this.element.attr('value');
|
||||
},
|
||||
_create: function(x) {
|
||||
debugPrint('ui.hifiRadioGroup._create', this.element[0]);
|
||||
this.initHifiControl();
|
||||
this.options.items = {
|
||||
hifiRadioButton: 'input[type=radio]',
|
||||
};
|
||||
this._super();
|
||||
// allow setting correct radio button by assign to .value property (or $.fn.val() etc.)
|
||||
Object.defineProperty(this.element[0], 'value', {
|
||||
set: function(nv) {
|
||||
try {
|
||||
this.radio('#' + nv).value(true);
|
||||
} catch (e) {}
|
||||
return this.value();
|
||||
}.bind(this),
|
||||
get: function() {
|
||||
return this.element.attr('value');
|
||||
}.bind(this),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// SPINNER (numeric input + up/down buttons)
|
||||
$.widget('ui.hifiSpinner', $.ui.spinner, {
|
||||
value: function value(nv) {
|
||||
if (arguments.length) {
|
||||
var num = parseFloat(nv);
|
||||
debugPrint('ui.hifiSpinner.value set', this.element[0].id, num, '(was: ' + this.value() + ')', 'raw:'+nv);
|
||||
this._value(num);
|
||||
this.element.change();
|
||||
}
|
||||
return parseFloat(this.element.val());
|
||||
},
|
||||
_value: function(value, allowAny) {
|
||||
debugPrint('ui.hifiSpinner._value', value, allowAny);
|
||||
return this._super(value, allowAny);
|
||||
},
|
||||
_create: function() {
|
||||
this.initHifiControl();
|
||||
var step = this.options.step = this.options.step || 1.0;
|
||||
// allow step=".01" for precision and data-step=".1" for default increment amount
|
||||
this.options.prescale = parseFloat(this.element.data('step') || step) / (step);
|
||||
this._super();
|
||||
this.previous = null;
|
||||
this.element.on('change._hifiSpinner', function() {
|
||||
var value = this.value(),
|
||||
invalid = !this.isValid();
|
||||
debugPrint('hifiSpinner.changed', value, invalid ? '!!!invalid' : 'valid');
|
||||
!invalid && this.element.attr('value', value);
|
||||
}.bind(this));
|
||||
},
|
||||
_spin: function( step, event ) {
|
||||
step = step * this.options.prescale * (
|
||||
event.shiftKey ? 0.1 : event.ctrlKey ? 10 : 1
|
||||
);
|
||||
return this._super( step, event );
|
||||
},
|
||||
_stop: function( event, ui ) {
|
||||
try {
|
||||
return this._super(event, ui);
|
||||
} finally {
|
||||
if (/mouse/.test(event && event.type)) {
|
||||
var value = this.value();
|
||||
if ((value || value === 0) && !isNaN(value) && this.previous !== null && this.previous !== value) {
|
||||
this.value(this.value());
|
||||
}
|
||||
this.previous = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
_format: function(n) {
|
||||
var precision = this._precision();
|
||||
return parseFloat(n).toFixed(precision);
|
||||
},
|
||||
_events: {
|
||||
mousewheel: function(event, delta) {
|
||||
if (document.activeElement === this.element[0]) {
|
||||
// fix broken mousewheel on Chrome / embedded webkit
|
||||
delta = delta === undefined ? -(event.originalEvent.deltaY+event.originalEvent.deltaX) : delta;
|
||||
$.ui.spinner.prototype._events.mousewheel.call(this, event, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// SLIDER
|
||||
$.widget('ui.hifiSlider', $.ui.slider, {
|
||||
value: function value(nv) {
|
||||
if (arguments.length) {
|
||||
var num = this._trimAlignValue(nv);
|
||||
debugPrint('hifiSlider.value', nv, num);
|
||||
if (this.options.value !== num) {
|
||||
this.options.value = num;
|
||||
this.element.change();
|
||||
}
|
||||
}
|
||||
return this.options.value;
|
||||
},
|
||||
_create: function() {
|
||||
this.initHifiControl();
|
||||
this._super();
|
||||
this.element
|
||||
.attr('type', this.element.attr('type') || 'slider')
|
||||
.find('.ui-slider-handle').html('<div class="inner-ui-slider-handle"></div>').end()
|
||||
.on('change', function() {
|
||||
this.hifiFindWidget('hifiSpinner').value(this.value());
|
||||
this._refresh();
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
238
unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js
Normal file
238
unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js
Normal file
|
@ -0,0 +1,238 @@
|
|||
// EnumMeta.js -- helper module that maps related enum values to printable names and ids
|
||||
|
||||
/* eslint-env commonjs */
|
||||
/* global DriveKeys, console */
|
||||
|
||||
var VERSION = '0.0.1';
|
||||
var WANT_DEBUG = false;
|
||||
|
||||
function _debugPrint() {
|
||||
// eslint-disable-next-line no-console
|
||||
(typeof Script === 'object' ? print : console.log)('EnumMeta | ' + [].slice.call(arguments).join(' '));
|
||||
}
|
||||
|
||||
var debugPrint = WANT_DEBUG ? _debugPrint : function(){};
|
||||
|
||||
try {
|
||||
/* global process */
|
||||
if (process.title === 'node') {
|
||||
_defineNodeJSMocks();
|
||||
}
|
||||
} catch (e) { /* eslint-disable-line empty-block */ }
|
||||
|
||||
_debugPrint(VERSION);
|
||||
|
||||
// FIXME: C++ emits this action event, but doesn't expose it yet to scripting
|
||||
// (ie: as Actions.ZOOM or Actions.TranslateCameraZ)
|
||||
var ACTION_TRANSLATE_CAMERA_Z = {
|
||||
actionName: 'TranslateCameraZ',
|
||||
actionID: 12,
|
||||
driveKeyName: 'ZOOM'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
version: VERSION,
|
||||
DriveKeyNames: invertKeys(DriveKeys),
|
||||
Controller: {
|
||||
Hardware: Object.keys(Controller.Hardware).reduce(function(names, prop) {
|
||||
names[prop+'Names'] = invertKeys(Controller.Hardware[prop]);
|
||||
return names;
|
||||
}, {}),
|
||||
ActionNames: _getActionNames(),
|
||||
StandardNames: invertKeys(Controller.Standard)
|
||||
},
|
||||
getDriveKeyNameFromActionName: getDriveKeyNameFromActionName,
|
||||
getActionNameFromDriveKeyName: getActionNameFromDriveKeyName,
|
||||
eventKeyText2KeyboardName: eventKeyText2KeyboardName,
|
||||
keyboardName2eventKeyText: keyboardName2eventKeyText,
|
||||
ACTION_TRANSLATE_CAMERA_Z: ACTION_TRANSLATE_CAMERA_Z,
|
||||
INVALID_ACTION_ID: Controller.findAction('INVALID_ACTION_ID_FOO')
|
||||
};
|
||||
|
||||
_debugPrint('///'+VERSION, Object.keys(module.exports));
|
||||
|
||||
var actionsMapping = {}, driveKeyMapping = {};
|
||||
|
||||
initializeMappings(actionsMapping, driveKeyMapping);
|
||||
|
||||
function invertKeys(object) {
|
||||
if (!object) {
|
||||
return object;
|
||||
}
|
||||
return Object.keys(object).reduce(function(out, key) {
|
||||
out[object[key]] = key;
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function _getActionNames() {
|
||||
var ActionNames = invertKeys(Controller.Hardware.Actions);
|
||||
ActionNames[ACTION_TRANSLATE_CAMERA_Z.actionID] = ACTION_TRANSLATE_CAMERA_Z.actionName;
|
||||
function mapActionName(actionName) {
|
||||
var actionKey = Controller.Hardware.Actions[actionName],
|
||||
actionID = Controller.findAction(actionName),
|
||||
similarName = eventKeyText2KeyboardName(actionName),
|
||||
existingName = ActionNames[actionID];
|
||||
|
||||
var keyName = actionName;
|
||||
|
||||
if (actionID === module.exports.INVALID_ACTION_ID) {
|
||||
_debugPrint('actionID === INVALID_ACTION_ID', actionName);
|
||||
}
|
||||
switch (actionName) {
|
||||
case 'StepTranslateX': actionName = 'StepTranslate'; break;
|
||||
case 'StepTranslateY': actionName = 'StepTranslate'; break;
|
||||
case 'StepTranslateZ': actionName = 'StepTranslate'; break;
|
||||
case 'ACTION1': actionName = 'PrimaryAction'; break;
|
||||
case 'ACTION2': actionName = 'SecondaryAction'; break;
|
||||
}
|
||||
debugPrint(keyName, actionName, actionKey, actionID);
|
||||
|
||||
similarName = similarName.replace('Lateral','Strafe').replace(/^(?:Longitudinal|Vertical)/, '');
|
||||
if (actionID in ActionNames) {
|
||||
// check if overlap is just BoomIn <=> BOOM_IN
|
||||
if (similarName !== existingName && actionName !== existingName) {
|
||||
throw new Error('assumption failed: overlapping actionID:'+JSON.stringify({
|
||||
actionID: actionID,
|
||||
actionKey: actionKey,
|
||||
actionName: actionName,
|
||||
similarName: similarName,
|
||||
keyName: keyName,
|
||||
existingName: existingName
|
||||
},0,2));
|
||||
}
|
||||
} else {
|
||||
ActionNames[actionID] = actionName;
|
||||
ActionNames[actionKey] = keyName;
|
||||
}
|
||||
}
|
||||
// first map non-legacy (eg: Up and not VERTICAL_UP) actions
|
||||
Object.keys(Controller.Hardware.Actions).filter(function(name) {
|
||||
return /[a-z]/.test(name);
|
||||
}).sort().reverse().forEach(mapActionName);
|
||||
// now legacy actions
|
||||
Object.keys(Controller.Hardware.Actions).filter(function(name) {
|
||||
return !/[a-z]/.test(name);
|
||||
}).sort().reverse().forEach(mapActionName);
|
||||
|
||||
return ActionNames;
|
||||
}
|
||||
|
||||
// attempts to brute-force translate an Action name into a DriveKey name
|
||||
// eg: _translateActionName('TranslateX') === 'TRANSLATE_X'
|
||||
// eg: _translateActionName('Yaw') === 'YAW'
|
||||
function _translateActionName(name, _index) {
|
||||
name = name || '';
|
||||
var key = name;
|
||||
var re = new RegExp('[A-Z][a-z0-9]+', 'g');
|
||||
key = key.replace(re, function(Word) {
|
||||
return Word.toUpperCase()+'_';
|
||||
})
|
||||
.replace(/_$/, '');
|
||||
|
||||
if (key in DriveKeys) {
|
||||
debugPrint('getDriveKeyFromEventName', _index, name, key, DriveKeys[key]);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function getActionNameFromDriveKeyName(driveKeyName) {
|
||||
return driveKeyMapping[driveKeyName];
|
||||
}
|
||||
// maps an action lookup value to a DriveKey name
|
||||
// eg: actionName: 'Yaw' === 'YAW'
|
||||
// actionKey: Controller.Actions.Yaw => 'YAW'
|
||||
// actionID: Controller.findAction('Yaw') => 'YAW'
|
||||
function getDriveKeyNameFromActionName(lookupValue) {
|
||||
if (lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionName ||
|
||||
lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionID) {
|
||||
return ACTION_TRANSLATE_CAMERA_Z.driveKeyName;
|
||||
}
|
||||
if (typeof lookupValue === 'string') {
|
||||
lookupValue = Controller.findAction(lookupValue);
|
||||
}
|
||||
return actionsMapping[lookupValue];
|
||||
}
|
||||
|
||||
// maps a Controller.key*Event event.text -> Controller.Hardware.Keyboard[name]
|
||||
// eg: ('Page Up') === 'PgUp'
|
||||
// eg: ('LEFT') === 'Left'
|
||||
function eventKeyText2KeyboardName(text) {
|
||||
if (eventKeyText2KeyboardName[text]) {
|
||||
// use memoized value
|
||||
return eventKeyText2KeyboardName[text];
|
||||
}
|
||||
var keyboardName = (text||'').toUpperCase().split(/[ _]/).map(function(WORD) {
|
||||
return WORD.replace(/([A-Z])(\w*)/g, function(_, A, b) {
|
||||
return (A.toUpperCase() + b.toLowerCase());
|
||||
});
|
||||
}).join('').replace('Page','Pg');
|
||||
return eventKeyText2KeyboardName[text] = eventKeyText2KeyboardName[keyboardName] = keyboardName;
|
||||
}
|
||||
|
||||
// maps a Controller.Hardware.Keyboard[name] -> Controller.key*Event event.text
|
||||
// eg: ('PgUp') === 'PAGE UP'
|
||||
// eg: ('Shift') === 'SHIFT'
|
||||
function keyboardName2eventKeyText(keyName) {
|
||||
if (keyboardName2eventKeyText[keyName]) {
|
||||
// use memoized value
|
||||
return keyboardName2eventKeyText[keyName];
|
||||
}
|
||||
var text = keyName.replace('Pg', 'Page');
|
||||
var caseWords = text.match(/[A-Z][a-z0-9]+/g) || [ text ];
|
||||
var eventText = caseWords.map(function(str) {
|
||||
return str.toUpperCase();
|
||||
}).join('_');
|
||||
return keyboardName2eventKeyText[keyName] = eventText;
|
||||
}
|
||||
|
||||
function initializeMappings(actionMap, driveKeyMap) {
|
||||
_debugPrint('creating mapping');
|
||||
var ref = ACTION_TRANSLATE_CAMERA_Z;
|
||||
actionMap[ref.actionName] = actionMap[ref.actionID] = ref.driveKeyName;
|
||||
actionMap.BoomIn = 'ZOOM';
|
||||
actionMap.BoomOut = 'ZOOM';
|
||||
|
||||
Controller.getActionNames().sort().reduce(
|
||||
function(out, name, index, arr) {
|
||||
var actionKey = arr[index];
|
||||
var actionID = Controller.findAction(name);
|
||||
var value = actionID in out ? out[actionID] : _translateActionName(name, index);
|
||||
if (value !== undefined) {
|
||||
var prefix = (actionID in out ? '+++' : '---');
|
||||
debugPrint(prefix + ' Action2DriveKeyName['+name+'('+actionID+')] = ' + value);
|
||||
driveKeyMap[value] = driveKeyMap[value] || name;
|
||||
}
|
||||
out[name] = out[actionID] = out[actionKey] = value;
|
||||
return out;
|
||||
}, actionMap);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// mocks for litmus testing using Node.js command line tools
|
||||
function _defineNodeJSMocks() {
|
||||
/* eslint-disable no-global-assign */
|
||||
DriveKeys = {
|
||||
TRANSLATE_X: 12345
|
||||
};
|
||||
Controller = {
|
||||
getActionNames: function() {
|
||||
return Object.keys(this.Hardware.Actions);
|
||||
},
|
||||
findAction: function(name) {
|
||||
return this.Hardware.Actions[name] || 4095;
|
||||
},
|
||||
Hardware: {
|
||||
Actions: {
|
||||
TranslateX: 54321
|
||||
},
|
||||
Application: {
|
||||
Grounded: 1
|
||||
},
|
||||
Keyboard: {
|
||||
A: 65
|
||||
}
|
||||
}
|
||||
};
|
||||
/* eslint-enable no-global-assign */
|
||||
}
|
457
unpublishedScripts/marketplace/camera-move/modules/_utils.js
Normal file
457
unpublishedScripts/marketplace/camera-move/modules/_utils.js
Normal file
|
@ -0,0 +1,457 @@
|
|||
// _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;
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
// config-utils.js -- helpers for coordinating Application runtime vs. Settings configuration
|
||||
//
|
||||
// * ApplicationConfig -- Menu items and API values.
|
||||
// * SettingsConfig -- scoped Settings values.
|
||||
|
||||
"use strict";
|
||||
/* eslint-env commonjs */
|
||||
/* global log */
|
||||
module.exports = {
|
||||
version: '0.0.1a',
|
||||
ApplicationConfig: ApplicationConfig,
|
||||
SettingsConfig: SettingsConfig
|
||||
};
|
||||
|
||||
var _utils = require('./_utils.js'),
|
||||
assert = _utils.assert;
|
||||
Object.assign = Object.assign || _utils.assign;
|
||||
|
||||
function _debugPrint() {
|
||||
print('config-utils | ' + [].slice.call(arguments).join(' '));
|
||||
}
|
||||
|
||||
var debugPrint = function() {};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Application-specific configuration values using runtime state / API props
|
||||
//
|
||||
// options.config[] supports the following item formats:
|
||||
// 'settingsName': { menu: 'Menu > MenuItem'}, // assumes MenuItem is a checkbox / checkable value
|
||||
// 'settingsName': { object: [ MyAvatar, 'property' ] },
|
||||
// 'settingsName': { object: [ MyAvatar, 'getterMethod', 'setterMethod' ] },
|
||||
// 'settingsName': { menu: 'Menu > MenuItem', object: [ MyAvatar, 'property' ] },
|
||||
// 'settingsName': { get: function getter() { ...}, set: function(nv) { ... } },
|
||||
|
||||
function ApplicationConfig(options) {
|
||||
options = options || {};
|
||||
assert('namespace' in options && 'config' in options);
|
||||
if (options.debug) {
|
||||
debugPrint = _debugPrint;
|
||||
debugPrint('debugPrinting enabled');
|
||||
}
|
||||
this.namespace = options.namespace;
|
||||
this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){});
|
||||
|
||||
this.config = {};
|
||||
this.register(options.config);
|
||||
}
|
||||
ApplicationConfig.prototype = {
|
||||
resolve: function resolve(key) {
|
||||
assert(typeof key === 'string', 'ApplicationConfig.resolve error: key is not a string: ' + key);
|
||||
if (0 !== key.indexOf('.') && !~key.indexOf('/')) {
|
||||
key = [ this.namespace, key ].join('/');
|
||||
}
|
||||
return (key in this.config) ? key : (debugPrint('ApplicationConfig -- could not resolve key: ' + key),undefined);
|
||||
},
|
||||
registerItem: function(settingName, item) {
|
||||
item._settingName = settingName;
|
||||
item.settingName = ~settingName.indexOf('/') ? settingName : [ this.namespace, settingName ].join('/');
|
||||
return this.config[item.settingName] = this.config[settingName] = new ApplicationConfigItem(item);
|
||||
},
|
||||
// process items into fully-qualfied ApplicationConfigItem instances
|
||||
register: function(items) {
|
||||
for (var p in items) {
|
||||
var item = items[p];
|
||||
item && this.registerItem(p, item);
|
||||
}
|
||||
},
|
||||
_getItem: function(key) {
|
||||
return this.config[this.resolve(key)];
|
||||
},
|
||||
getValue: function(key, defaultValue) {
|
||||
var item = this._getItem(key);
|
||||
if (!item) {
|
||||
return defaultValue;
|
||||
}
|
||||
return item.get();
|
||||
},
|
||||
setValue: function setValue(key, value) {
|
||||
key = this.resolve(key);
|
||||
var lastValue = this.getValue(key, value);
|
||||
var ret = this._getItem(key).set(value);
|
||||
if (lastValue !== value) {
|
||||
this.valueUpdated(key, value, lastValue, 'ApplicationConfig.setValue');
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
// sync dual-source (ie: Menu + API) items
|
||||
resyncValue: function(key) {
|
||||
var item = this._getItem(key);
|
||||
return item && item.resync();
|
||||
},
|
||||
// sync Settings values -> Application state
|
||||
applyValue: function applyValue(key, value, origin) {
|
||||
if (this.resolve(key)) {
|
||||
var appValue = this.getValue(key, value);
|
||||
debugPrint('applyValue', key, value, origin ? '['+origin+']' : '', appValue);
|
||||
if (appValue !== value) {
|
||||
this.setValue(key, value);
|
||||
debugPrint('applied new setting', key, value, '(was:'+appValue+')');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ApplicationConfigItem represents a single API/Menu item accessor
|
||||
function ApplicationConfigItem(item) {
|
||||
Object.assign(this, item);
|
||||
Object.assign(this, {
|
||||
_item: item.get && item,
|
||||
_object: this._parseObjectConfig(this.object),
|
||||
_menu: this._parseMenuConfig(this.menu)
|
||||
});
|
||||
this.authority = this._item ? 'item' : this._object ? 'object' : this._menu ? 'menu' : null;
|
||||
this._authority = this['_'+this.authority];
|
||||
debugPrint('_authority', this.authority, this._authority, Object.keys(this._authority));
|
||||
assert(this._authority, 'expected item.get, .object or .menu definition; ' + this.settingName);
|
||||
}
|
||||
ApplicationConfigItem.prototype = {
|
||||
resync: function resync() {
|
||||
var authoritativeValue = this._authority.get();
|
||||
if (this._menu && this._menu.get() !== authoritativeValue) {
|
||||
_debugPrint(this.settingName, this._menu.menuItem,
|
||||
'... menu value ('+this._menu.get()+') out of sync;',
|
||||
'setting to authoritativeValue ('+authoritativeValue+')');
|
||||
this._menu.set(authoritativeValue);
|
||||
}
|
||||
if (this._object && this._object.get() !== authoritativeValue) {
|
||||
_debugPrint(this.settingName, this._object.getter || this._object.property,
|
||||
'... object value ('+this._object.get()+') out of sync;',
|
||||
'setting to authoritativeValue ('+authoritativeValue+')');
|
||||
this._object.set(authoritativeValue);
|
||||
}
|
||||
},
|
||||
toString: function() {
|
||||
return '[ApplicationConfigItem ' + [
|
||||
'setting:' + JSON.stringify(this.settingName),
|
||||
'authority:' + JSON.stringify(this.authority),
|
||||
this._object && 'object:' + JSON.stringify(this._object.property || this._object.getter),
|
||||
this._menu && 'menu:' + JSON.stringify(this._menu.menu)
|
||||
].filter(Boolean).join(' ') + ']';
|
||||
},
|
||||
get: function get() {
|
||||
return this._authority.get();
|
||||
},
|
||||
set: function set(nv) {
|
||||
this._object && this._object.set(nv);
|
||||
this._menu && this._menu.set(nv);
|
||||
return nv;
|
||||
},
|
||||
_raiseError: function(errorMessage) {
|
||||
if (this.debug) {
|
||||
throw new Error(errorMessage);
|
||||
} else {
|
||||
_debugPrint('ERROR: ' + errorMessage);
|
||||
}
|
||||
},
|
||||
_parseObjectConfig: function(parts) {
|
||||
if (!Array.isArray(parts) || parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
var object = parts[0], getter = parts[1], setter = parts[2];
|
||||
if (typeof object[getter] === 'function' && typeof object[setter] === 'function') {
|
||||
// [ API, 'getter', 'setter' ]
|
||||
return {
|
||||
object: object, getter: getter, setter: setter,
|
||||
get: function getObjectValue() {
|
||||
return this.object[this.getter]();
|
||||
},
|
||||
set: function setObjectValue(nv) {
|
||||
return this.object[this.setter](nv), nv;
|
||||
}
|
||||
};
|
||||
} else if (getter in object) {
|
||||
// [ API, 'property' ]
|
||||
return {
|
||||
object: object, property: getter,
|
||||
get: function() {
|
||||
return this.object[this.property];
|
||||
},
|
||||
set: function(nv) {
|
||||
return this.object[this.property] = nv;
|
||||
}
|
||||
};
|
||||
}
|
||||
this._raiseError('{ object: [ Object, getterOrPropertyName, setterName ] } -- invalid params or does not exist: ' +
|
||||
[ this.settingName, this.object, getter, setter ].join(' | '));
|
||||
},
|
||||
_parseMenuConfig: function(menu) {
|
||||
if (!menu || typeof menu !== 'string') {
|
||||
return null;
|
||||
}
|
||||
var parts = menu.split(/\s*>\s*/), menuItemName = parts.pop(), menuName = parts.join(' > ');
|
||||
if (menuItemName && Menu.menuItemExists(menuName, menuItemName)) {
|
||||
return {
|
||||
menu: menu, menuName: menuName, menuItemName: menuItemName,
|
||||
get: function() {
|
||||
return Menu.isOptionChecked(this.menuItemName);
|
||||
},
|
||||
set: function(nv) {
|
||||
return Menu.setIsOptionChecked(this.menuItemName, nv), nv;
|
||||
}
|
||||
};
|
||||
}
|
||||
this._raiseError('{ menu: "Menu > Item" } structure -- invalid params or does not exist: ' +
|
||||
[ this.settingName, this.menu, menuName, menuItemName ].join(' | '));
|
||||
}
|
||||
}; // ApplicationConfigItem.prototype
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// grouped configuration using the Settings.* API
|
||||
function SettingsConfig(options) {
|
||||
options = options || {};
|
||||
assert('namespace' in options);
|
||||
this.namespace = options.namespace;
|
||||
this.defaultValues = {};
|
||||
this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){});
|
||||
if (options.defaultValues) {
|
||||
Object.keys(options.defaultValues)
|
||||
.forEach(_utils.bind(this, function(key) {
|
||||
var fullSettingsKey = this.resolve(key);
|
||||
this.defaultValues[fullSettingsKey] = options.defaultValues[key];
|
||||
}));
|
||||
}
|
||||
}
|
||||
SettingsConfig.prototype = {
|
||||
resolve: function(key) {
|
||||
assert(typeof key === 'string', 'SettingsConfig.resolve error: key is not a string: ' + key);
|
||||
return (0 !== key.indexOf('.') && !~key.indexOf('/')) ?
|
||||
[ this.namespace, key ].join('/') : key;
|
||||
},
|
||||
getValue: function(key, defaultValue) {
|
||||
key = this.resolve(key);
|
||||
defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue;
|
||||
return Settings.getValue(key, defaultValue);
|
||||
},
|
||||
setValue: function setValue(key, value) {
|
||||
key = this.resolve(key);
|
||||
var lastValue = this.getValue(key);
|
||||
var ret = Settings.setValue(key, value);
|
||||
if (lastValue !== value) {
|
||||
this.valueUpdated(key, value, lastValue, 'SettingsConfig.setValue');
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
getFloat: function getFloat(key, defaultValue) {
|
||||
key = this.resolve(key);
|
||||
defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue;
|
||||
var value = parseFloat(this.getValue(key, defaultValue));
|
||||
return isFinite(value) ? value : isFinite(defaultValue) ? defaultValue : 0.0;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,324 @@
|
|||
// CustomSettingsApp.js -- manages Settings between a Client script and connected "settings" tablet WebView page
|
||||
// see browser/BridgedSettings.js for the webView side
|
||||
|
||||
// example:
|
||||
// var button = tablet.addButton({ text: 'My Settings' });
|
||||
// var mySettingsApp = new CustomSettingsApp({
|
||||
// namespace: 'mySettingsGroup',
|
||||
// url: Script.resolvePath('myapp.html'),
|
||||
// uuid: button.uuid,
|
||||
// tablet: tablet
|
||||
// });
|
||||
//
|
||||
// // settings are automatically sync'd from the web page back to Interface; to be notified when that happens, use:
|
||||
// myAppSettings.valueUpdated.connect(function(name, value, oldValue, origin) {
|
||||
// print('setting updated from web page', name, value, oldValue, origin);
|
||||
// });
|
||||
//
|
||||
// // settings are also automatically sync'd from Interface back to the web page; to manually sync a value, use:
|
||||
// myAppSettings.syncValue(fullSettingsKey, value);
|
||||
|
||||
/* eslint-env commonjs */
|
||||
"use strict";
|
||||
|
||||
CustomSettingsApp.version = '0.0.0';
|
||||
module.exports = CustomSettingsApp;
|
||||
|
||||
var _utils = require('../_utils.js');
|
||||
Object.assign = Object.assign || _utils.assign;
|
||||
|
||||
function assert(truthy, message) {
|
||||
return _utils.assert.call(this, truthy, 'CustomSettingsApp | ' + message);
|
||||
}
|
||||
|
||||
function _debugPrint() {
|
||||
print('CustomSettingsApp | ' + [].slice.call(arguments).join(' '));
|
||||
}
|
||||
var debugPrint = function() {};
|
||||
|
||||
function CustomSettingsApp(options) {
|
||||
assert('url' in options, 'expected options.url');
|
||||
if (options.debug) {
|
||||
debugPrint = _debugPrint;
|
||||
}
|
||||
|
||||
this.url = options.url;
|
||||
this.namespace = options.namespace || 'BridgedSettings';
|
||||
this.uuid = options.uuid || Uuid.generate();
|
||||
this.recheckInterval = options.recheckInterval || 1000;
|
||||
|
||||
this.settingsScreenVisible = false;
|
||||
this.isActive = false;
|
||||
|
||||
this.extraParams = Object.assign(options.extraParams || {}, {
|
||||
customSettingsVersion: CustomSettingsApp.version+'',
|
||||
protocolVersion: location.protocolVersion && location.protocolVersion()
|
||||
});
|
||||
|
||||
var params = {
|
||||
namespace: this.namespace,
|
||||
uuid: this.uuid,
|
||||
debug: options.debug || undefined
|
||||
};
|
||||
|
||||
// encode PARAMS into '?key=value&...'
|
||||
var query = Object.keys(params).map(function encodeValue(key) {
|
||||
var value = encodeURIComponent(params[key] === undefined ? '' : params[key]);
|
||||
return [ key, value ].join('=');
|
||||
}).join('&');
|
||||
this.url += '?&' + query;
|
||||
|
||||
this.isActiveChanged = _utils.signal(function(isActive) {});
|
||||
this.valueUpdated = _utils.signal(function(key, value, oldValue, origin) {});
|
||||
|
||||
this.settingsAPI = options.settingsAPI || Settings;
|
||||
|
||||
// keep track of accessed settings so they can be kept in sync if changed externally
|
||||
this._activeSettings = { sent: {}, received: {}, remote: {} };
|
||||
|
||||
if (options.tablet) {
|
||||
this._initialize(options.tablet);
|
||||
}
|
||||
}
|
||||
|
||||
CustomSettingsApp.prototype = {
|
||||
tablet: null,
|
||||
resolve: function(key) {
|
||||
if (0 === key.indexOf('.') || ~key.indexOf('/')) {
|
||||
// key is already qualified under a group; return as-is
|
||||
return key;
|
||||
}
|
||||
// nest under the current namespace
|
||||
return [ this.namespace, key ].join('/');
|
||||
},
|
||||
sendEvent: function(msg) {
|
||||
assert(this.tablet, '!this.tablet');
|
||||
msg.ns = msg.ns || this.namespace;
|
||||
msg.uuid = msg.uuid || this.uuid;
|
||||
this.tablet.emitScriptEvent(JSON.stringify(msg));
|
||||
},
|
||||
getValue: function(key, defaultValue) {
|
||||
key = this.resolve(key);
|
||||
return key in this._activeSettings.remote ? this._activeSettings.remote[key] : defaultValue;
|
||||
},
|
||||
setValue: function(key, value) {
|
||||
key = this.resolve(key);
|
||||
var current = this.getValue(key);
|
||||
if (current !== value) {
|
||||
return this.syncValue(key, value, 'CustomSettingsApp.setValue');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
syncValue: function(key, value, origin) {
|
||||
key = this.resolve(key);
|
||||
var oldValue = this._activeSettings.remote[key];
|
||||
assert(value !== null, 'CustomSettingsApp.syncValue value is null');
|
||||
this.sendEvent({ id: 'valueUpdated', params: [key, value, oldValue, origin] });
|
||||
this._activeSettings.sent[key] = value;
|
||||
this._activeSettings.remote[key] = value;
|
||||
this.valueUpdated(key, value, oldValue, (origin ? origin+':' : '') + 'CustomSettingsApp.syncValue');
|
||||
},
|
||||
onScreenChanged: function onScreenChanged(type, url) {
|
||||
this.settingsScreenVisible = (url === this.url);
|
||||
debugPrint('===> onScreenChanged', type, url, 'settingsScreenVisible: ' + this.settingsScreenVisible);
|
||||
if (this.isActive && !this.settingsScreenVisible) {
|
||||
this.isActiveChanged(this.isActive = false);
|
||||
}
|
||||
},
|
||||
|
||||
_apiGetValue: function(key, defaultValue) {
|
||||
// trim rooted keys like "/desktopTabletBecomesToolbar" => "desktopTabletBecomesToolbar"
|
||||
key = key.replace(/^\//,'');
|
||||
return this.settingsAPI.getValue(key, defaultValue);
|
||||
},
|
||||
_apiSetValue: function(key, value) {
|
||||
key = key.replace(/^\//,'');
|
||||
return this.settingsAPI.setValue(key, value);
|
||||
},
|
||||
_setValue: function(key, value, oldValue, origin) {
|
||||
var current = this._apiGetValue(key),
|
||||
lastRemoteValue = this._activeSettings.remote[key];
|
||||
debugPrint('.setValue(' + JSON.stringify({key: key, value: value, current: current, lastRemoteValue: lastRemoteValue })+')');
|
||||
this._activeSettings.received[key] = value;
|
||||
this._activeSettings.remote[key] = value;
|
||||
var result;
|
||||
if (lastRemoteValue !== value) {
|
||||
this.valueUpdated(key, value, lastRemoteValue, 'CustomSettingsApp.tablet');
|
||||
}
|
||||
if (current !== value) {
|
||||
result = this._apiSetValue(key, value);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
_handleValidatedMessage: function(obj, msg) {
|
||||
var tablet = this.tablet;
|
||||
if (!tablet) {
|
||||
throw new Error('_handleValidatedMessage called when not connected to tablet...');
|
||||
}
|
||||
var params = Array.isArray(obj.params) ? obj.params : [obj.params];
|
||||
var parts = (obj.method||'').split('.'), api = parts[0], method = parts[1];
|
||||
switch(api) {
|
||||
case 'valueUpdated': obj.result = this._setValue.apply(this, params); break;
|
||||
case 'Settings':
|
||||
if (method && params[0]) {
|
||||
var key = this.resolve(params[0]), value = params[1];
|
||||
debugPrint('>>>>', method, key, value);
|
||||
switch (method) {
|
||||
case 'getValue':
|
||||
obj.result = this._apiGetValue(key, value);
|
||||
this._activeSettings.sent[key] = obj.result;
|
||||
this._activeSettings.remote[key] = obj.result;
|
||||
break;
|
||||
case 'setValue':
|
||||
obj.result = this._setValue(key, value, params[2], params[3]);
|
||||
break;
|
||||
default:
|
||||
obj.error = 'unmapped Settings method: ' + method;
|
||||
throw new Error(obj.error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: if (this.onUnhandledMessage) {
|
||||
this.onUnhandledMessage(obj, msg);
|
||||
} else {
|
||||
obj.error = 'unmapped method call: ' + msg;
|
||||
}
|
||||
}
|
||||
if (obj.id) {
|
||||
// if message has an id, reply with the same message obj which now has a .result or .error field
|
||||
// note: a small delay is needed because of an apparent race condition between ScriptEngine and Tablet WebViews
|
||||
Script.setTimeout(_utils.bind(this, function() {
|
||||
this.sendEvent(obj);
|
||||
}), 100);
|
||||
} else if (obj.error) {
|
||||
throw new Error(obj.error);
|
||||
}
|
||||
},
|
||||
onWebEventReceived: function onWebEventReceived(msg) {
|
||||
debugPrint('onWebEventReceived', msg);
|
||||
var tablet = this.tablet;
|
||||
if (!tablet) {
|
||||
throw new Error('onWebEventReceived called when not connected to tablet...');
|
||||
}
|
||||
if (msg === this.url) {
|
||||
if (this.isActive) {
|
||||
// user (or page) refreshed the web view; trigger !isActive so client script can perform cleanup
|
||||
this.isActiveChanged(this.isActive = false);
|
||||
}
|
||||
this.isActiveChanged(this.isActive = true);
|
||||
// reply to initial HTML page ACK with any extraParams that were specified
|
||||
this.sendEvent({ id: 'extraParams', extraParams: this.extraParams });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var obj = assert(JSON.parse(msg));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (obj.ns === this.namespace && obj.uuid === this.uuid) {
|
||||
debugPrint('valid onWebEventReceived', msg);
|
||||
this._handleValidatedMessage(obj, msg);
|
||||
}
|
||||
},
|
||||
|
||||
_initialize: function(tablet) {
|
||||
if (this.tablet) {
|
||||
throw new Error('CustomSettingsApp._initialize called but this.tablet already has a value');
|
||||
}
|
||||
this.tablet = tablet;
|
||||
tablet.webEventReceived.connect(this, 'onWebEventReceived');
|
||||
tablet.screenChanged.connect(this, 'onScreenChanged');
|
||||
|
||||
this.onAPIValueUpdated = function(key, newValue, oldValue, origin) {
|
||||
if (this._activeSettings.remote[key] !== newValue) {
|
||||
_debugPrint('onAPIValueUpdated: ' + key + ' = ' + JSON.stringify(newValue),
|
||||
'(was: ' + JSON.stringify(oldValue) +')');
|
||||
this.syncValue(key, newValue, (origin ? origin+':' : '') + 'CustomSettingsApp.onAPIValueUpdated');
|
||||
}
|
||||
};
|
||||
this.isActiveChanged.connect(this, function(isActive) {
|
||||
this._activeSettings.remote = {}; // reset assumptions about remote values
|
||||
isActive ? this.$startMonitor() : this.$stopMonitor();
|
||||
});
|
||||
debugPrint('CustomSettingsApp...initialized', this.namespace);
|
||||
},
|
||||
|
||||
$syncSettings: function() {
|
||||
for (var p in this._activeSettings.sent) {
|
||||
var value = this._apiGetValue(p),
|
||||
lastValue = this._activeSettings.remote[p];
|
||||
if (value !== undefined && value !== null && value !== '' && value !== lastValue) {
|
||||
_debugPrint('CustomSettingsApp... detected external settings change', p, value);
|
||||
this.syncValue(p, value, 'Settings');
|
||||
this.valueUpdated(p, value, lastValue, 'CustomSettingsApp.$syncSettings');
|
||||
}
|
||||
}
|
||||
},
|
||||
$startMonitor: function() {
|
||||
if (!(this.recheckInterval > 0)) {
|
||||
_debugPrint('$startMonitor -- recheckInterval <= 0; not starting settings monitor thread');
|
||||
return false;
|
||||
}
|
||||
if (this.interval) {
|
||||
this.$stopMonitor();
|
||||
}
|
||||
if (this.settingsAPI.valueUpdated) {
|
||||
_debugPrint('settingsAPI supports valueUpdated -- binding to detect settings changes', this.settingsAPI);
|
||||
this.settingsAPI.valueUpdated.connect(this, 'onAPIValueUpdated');
|
||||
}
|
||||
this.interval = Script.setInterval(_utils.bind(this, '$syncSettings'), this.recheckInterval);
|
||||
_debugPrint('STARTED MONITORING THREAD');
|
||||
},
|
||||
$stopMonitor: function() {
|
||||
if (this.interval) {
|
||||
Script.clearInterval(this.interval);
|
||||
this.interval = 0;
|
||||
if (this.settingsAPI.valueUpdated) {
|
||||
this.settingsAPI.valueUpdated.disconnect(this, 'onAPIValueUpdated');
|
||||
}
|
||||
_debugPrint('stopped monitoring thread');
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
cleanup: function() {
|
||||
if (!this.tablet) {
|
||||
return _debugPrint('CustomSettingsApp...cleanup called when not initialized');
|
||||
}
|
||||
var tablet = this.tablet;
|
||||
tablet.webEventReceived.disconnect(this, 'onWebEventReceived');
|
||||
tablet.screenChanged.disconnect(this, 'onScreenChanged');
|
||||
this.$stopMonitor();
|
||||
if (this.isActive) {
|
||||
try {
|
||||
this.isActiveChanged(this.isActive = false);
|
||||
} catch (e) {
|
||||
_debugPrint('ERROR: cleanup error during isActiveChanged(false)', e);
|
||||
}
|
||||
}
|
||||
this.toggle(false);
|
||||
this.settingsScreenVisible = false;
|
||||
this.tablet = null;
|
||||
debugPrint('cleanup completed', this.namespace);
|
||||
},
|
||||
|
||||
toggle: function(show) {
|
||||
if (!this.tablet) {
|
||||
return _debugPrint('CustomSettingsApp...cleanup called when not initialized');
|
||||
}
|
||||
if (typeof show !== 'boolean') {
|
||||
show = !this.settingsScreenVisible;
|
||||
}
|
||||
|
||||
if (this.settingsScreenVisible && !show) {
|
||||
this.tablet.gotoHomeScreen();
|
||||
} else if (!this.settingsScreenVisible && show) {
|
||||
Script.setTimeout(_utils.bind(this, function() {
|
||||
// Interface sometimes crashes if not for adding a small timeout here :(
|
||||
this.tablet.gotoWebScreen(this.url);
|
||||
}), 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
// BridgedSettings.js -- HTML-side implementation of bridged/async Settings
|
||||
// see ../CustomSettingsApp.js for the corresponding Interface script
|
||||
|
||||
/* eslint-env commonjs, browser */
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
|
||||
(function(global) {
|
||||
"use strict";
|
||||
|
||||
BridgedSettings.version = '0.0.2';
|
||||
|
||||
try {
|
||||
module.exports = BridgedSettings;
|
||||
} catch (e) {
|
||||
global.BridgedSettings = BridgedSettings;
|
||||
}
|
||||
|
||||
var _utils = global._utils || (typeof require === 'function' && require('../../_utils.js'));
|
||||
if (!_utils || !_utils.signal) {
|
||||
throw new Error('html.BridgedSettings.js -- expected _utils to be available on the global object (ie: window._utils)');
|
||||
}
|
||||
var signal = _utils.signal;
|
||||
|
||||
function log() {
|
||||
console.info('bridgedSettings | ' + [].slice.call(arguments).join(' ')); // eslint-disable-line no-console
|
||||
}
|
||||
log('version', BridgedSettings.version);
|
||||
|
||||
var debugPrint = function() {}; // = log
|
||||
|
||||
function BridgedSettings(options) {
|
||||
options = options || {};
|
||||
// Note: Interface changed how window.EventBridge behaves again; it now arbitrarily replaces the global value
|
||||
// sometime after the initial page load, invaliding any held references to it.
|
||||
// As a workaround this proxies the local property to the current global value.
|
||||
var _lastEventBridge = global.EventBridge;
|
||||
Object.defineProperty(this, 'eventBridge', { enumerable: true, get: function() {
|
||||
if (_lastEventBridge !== global.EventBridge) {
|
||||
log('>>> EventBridge changed in-flight', '(was: ' + _lastEventBridge + ' | is: ' + global.EventBridge + ')');
|
||||
_lastEventBridge = global.EventBridge;
|
||||
}
|
||||
return global.EventBridge;
|
||||
}});
|
||||
Object.assign(this, {
|
||||
//eventBridge: options.eventBridge || global.EventBridge,
|
||||
namespace: options.namespace || 'BridgedSettings',
|
||||
uuid: options.uuid || undefined,
|
||||
valueReceived: signal(function valueReceived(key, newValue, oldValue, origin){}),
|
||||
callbackError: signal(function callbackError(error, message){}),
|
||||
pendingRequestsFinished: signal(function pendingRequestsFinished(){}),
|
||||
extraParams: options.extraParams || {},
|
||||
_hifiValues: {},
|
||||
|
||||
debug: options.debug,
|
||||
log: log.bind({}, options.namespace + ' |'),
|
||||
debugPrint: function() {
|
||||
return this.debug && this.log.apply(this, arguments);
|
||||
},
|
||||
_boundScriptEventReceived: this.onScriptEventReceived.bind(this),
|
||||
callbacks: Object.defineProperties(options.callbacks || {}, {
|
||||
extraParams: { value: this.handleExtraParams },
|
||||
valueUpdated: { value: this.handleValueUpdated },
|
||||
})
|
||||
});
|
||||
this.log('connecting to EventBridge.scriptEventReceived');
|
||||
this.eventBridge.scriptEventReceived.connect(this._boundScriptEventReceived);
|
||||
}
|
||||
|
||||
BridgedSettings.prototype = {
|
||||
_callbackId: 1,
|
||||
toString: function() {
|
||||
return '[BridgedSettings namespace='+this.namespace+']';
|
||||
},
|
||||
resolve: function(key) {
|
||||
if (0 !== key.indexOf('.') && !~key.indexOf('/')) {
|
||||
return [ this.namespace, key ].join('/');
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
},
|
||||
handleValueUpdated: function(msg) {
|
||||
// client script notified us that a value was updated on that side
|
||||
var key = this.resolve(msg.params[0]),
|
||||
value = msg.params[1],
|
||||
oldValue = msg.params[2],
|
||||
origin = msg.params[3];
|
||||
log('callbacks.valueUpdated', key, value, oldValue, origin);
|
||||
this._hifiValues[key] = value;
|
||||
this.valueReceived(key, value, oldValue, (origin?origin+':':'') + 'callbacks.valueUpdated');
|
||||
},
|
||||
handleExtraParams: function(msg) {
|
||||
// client script sent us extraParams
|
||||
var extraParams = msg.extraParams;
|
||||
var previousParams = JSON.parse(JSON.stringify(this.extraParams));
|
||||
|
||||
_utils.sortedAssign(this.extraParams, extraParams);
|
||||
|
||||
this._hifiValues['.extraParams'] = this.extraParams;
|
||||
this.debugPrint('received .extraParams', JSON.stringify(extraParams,0,2));
|
||||
this.valueReceived('.extraParams', this.extraParams, previousParams, 'html.bridgedSettings.handleExtraParams');
|
||||
},
|
||||
cleanup: function() {
|
||||
try {
|
||||
this.eventBridge.scriptEventReceived.disconnect(this._boundScriptEventReceived);
|
||||
} catch (e) {
|
||||
this.log('error disconnecting from scriptEventReceived:', e);
|
||||
}
|
||||
},
|
||||
pendingRequestCount: function() {
|
||||
return Object.keys(this.callbacks).length;
|
||||
},
|
||||
_handleValidatedMessage: function(obj, msg) {
|
||||
var callback = this.callbacks[obj.id];
|
||||
if (callback) {
|
||||
try {
|
||||
return callback.call(this, obj) || true;
|
||||
} catch (e) {
|
||||
this.log('CALLBACK ERROR', this.namespace, obj.id, '_onScriptEventReceived', e);
|
||||
this.callbackError(e, obj);
|
||||
if (this.debug) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else if (this.onUnhandledMessage) {
|
||||
return this.onUnhandledMessage(obj, msg);
|
||||
}
|
||||
},
|
||||
onScriptEventReceived: function(msg) {
|
||||
this.debugPrint(this.namespace, '_onScriptEventReceived......' + msg);
|
||||
try {
|
||||
var obj = JSON.parse(msg);
|
||||
var validSender = obj.ns === this.namespace && obj.uuid === this.uuid;
|
||||
if (validSender) {
|
||||
return this._handleValidatedMessage(obj, msg);
|
||||
} else {
|
||||
debugPrint('xskipping', JSON.stringify([obj.ns, obj.uuid]), JSON.stringify(this), msg);
|
||||
}
|
||||
} catch (e) {
|
||||
log('rpc error:', e, msg);
|
||||
return e;
|
||||
}
|
||||
},
|
||||
sendEvent: function(msg) {
|
||||
msg.ns = msg.ns || this.namespace;
|
||||
msg.uuid = msg.uuid || this.uuid;
|
||||
debugPrint('sendEvent', JSON.stringify(msg));
|
||||
this.eventBridge.emitWebEvent(JSON.stringify(msg));
|
||||
},
|
||||
getValue: function(key, defaultValue) {
|
||||
key = this.resolve(key);
|
||||
return key in this._hifiValues ? this._hifiValues[key] : defaultValue;
|
||||
},
|
||||
setValue: function(key, value) {
|
||||
key = this.resolve(key);
|
||||
var current = this.getValue(key);
|
||||
if (current !== value) {
|
||||
debugPrint('SET VALUE : ' + JSON.stringify({ key: key, current: current, value: value }));
|
||||
return this.syncValue(key, value, 'setValue');
|
||||
}
|
||||
this._hifiValues[key] = value;
|
||||
return false;
|
||||
},
|
||||
syncValue: function(key, value, origin) {
|
||||
return this.sendEvent({ method: 'valueUpdated', params: [key, value, this.getValue(key), origin] });
|
||||
},
|
||||
getValueAsync: function(key, defaultValue, callback) {
|
||||
key = this.resolve(key);
|
||||
if (typeof defaultValue === 'function') {
|
||||
callback = defaultValue;
|
||||
defaultValue = undefined;
|
||||
}
|
||||
var params = defaultValue !== undefined ? [ key, defaultValue ] : [ key ],
|
||||
event = { method: 'Settings.getValue', params: params };
|
||||
|
||||
this.debugPrint('< getValueAsync...', key, params);
|
||||
if (callback) {
|
||||
event.id = this._callbackId++;
|
||||
this.callbacks[event.id] = function(obj) {
|
||||
try {
|
||||
callback(obj.error, obj.result);
|
||||
if (!obj.error) {
|
||||
this._hifiValues[key] = obj.result;
|
||||
}
|
||||
} finally {
|
||||
delete this.callbacks[event.id];
|
||||
}
|
||||
if (this.pendingRequestCount() === 0) {
|
||||
setTimeout(function() {
|
||||
this.pendingRequestsFinished();
|
||||
}.bind(this), 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
this.sendEvent(event);
|
||||
},
|
||||
};
|
||||
})(this);
|
|
@ -0,0 +1,195 @@
|
|||
// JQuerySettings.js -- HTML-side helper class for managing settings-linked jQuery UI elements
|
||||
|
||||
/* eslint-env jquery, commonjs, browser */
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
/* global assert, log */
|
||||
// ----------------------------------------------------------------------------
|
||||
(function(global) {
|
||||
"use strict";
|
||||
|
||||
JQuerySettings.version = '0.0.0';
|
||||
|
||||
try {
|
||||
module.exports = JQuerySettings;
|
||||
} catch (e) {
|
||||
global.JQuerySettings= JQuerySettings;
|
||||
}
|
||||
|
||||
var _utils = global._utils || (typeof require === 'function' && require('../../_utils.js'));
|
||||
|
||||
if (!_utils || !_utils.signal) {
|
||||
throw new Error('html.JQuerySettings.js -- expected _utils to be available on the global object (ie: window._utils)'+module);
|
||||
}
|
||||
var signal = _utils.signal,
|
||||
assert = _utils.assert;
|
||||
|
||||
function log() {
|
||||
console.info('jquerySettings | ' + [].slice.call(arguments).join(' ')); // eslint-disable-line no-console
|
||||
}
|
||||
log('version', JQuerySettings.version);
|
||||
|
||||
var debugPrint = function() {}; // = log
|
||||
|
||||
function JQuerySettings(options) {
|
||||
assert('namespace' in options);
|
||||
|
||||
Object.assign(this, {
|
||||
id2Setting: {}, // DOM id -> qualified Settings key
|
||||
Setting2id: {}, // qualified Settings key -> DOM id
|
||||
observers: {}, // DOM MutationObservers
|
||||
mutationEvent: signal(function mutationEvent(event) {}),
|
||||
boundOnDOMMutation: this._onDOMMutation.bind(this),
|
||||
}, options);
|
||||
}
|
||||
JQuerySettings.idCounter = 0;
|
||||
JQuerySettings.prototype = {
|
||||
toString: function() {
|
||||
return '[JQuerySettings namespace='+this.namespace+']';
|
||||
},
|
||||
mutationConfig: {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
attributeFilter: [ 'value', 'checked', 'data-checked', 'data-value' ]
|
||||
},
|
||||
_onDOMMutation: function(mutations, observer) {
|
||||
mutations.forEach(function(mutation, index) {
|
||||
var target = mutation.target,
|
||||
targetId = target.dataset['for'] || target.id,
|
||||
domId = target.id,
|
||||
attrValue = target.getAttribute(mutation.attributeName),
|
||||
hifiType = target.dataset.hifiType,
|
||||
value = hifiType ? $(target)[hifiType]('instance').value() : attrValue,
|
||||
oldValue = mutation.oldValue;
|
||||
var event = {
|
||||
key: this.getKey(targetId, true) || this.getKey(domId),
|
||||
value: value,
|
||||
oldValue: oldValue,
|
||||
hifiType: hifiType,
|
||||
domId: domId,
|
||||
domType: target.getAttribute('type') || target.type,
|
||||
targetId: targetId,
|
||||
attrValue: attrValue,
|
||||
domName: target.name,
|
||||
type: mutation.type,
|
||||
};
|
||||
|
||||
switch (typeof value) {
|
||||
case 'boolean': event.oldValue = !!event.oldValue; break;
|
||||
case 'number':
|
||||
var tmp = parseFloat(oldValue);
|
||||
if (isFinite(tmp)) {
|
||||
event.oldValue = tmp;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return (event.oldValue === event.value) ?
|
||||
debugPrint('SKIP NON-MUTATION', event.key, event.hifiType) :
|
||||
this.mutationEvent(event);
|
||||
}.bind(this));
|
||||
},
|
||||
observeNode: function(node) {
|
||||
assert(node.id);
|
||||
var observer = this.observers[node.id];
|
||||
if (!observer) {
|
||||
observer = new MutationObserver(this.boundOnDOMMutation);
|
||||
observer.observe(node, this.mutationConfig);
|
||||
this.observers[node.id] = observer;
|
||||
}
|
||||
debugPrint('observeNode', node.id, node.dataset.hifiType, node.name);
|
||||
return observer;
|
||||
},
|
||||
resolve: function(key) {
|
||||
assert(typeof key === 'string');
|
||||
if (0 !== key.indexOf('.') && !~key.indexOf('/')) {
|
||||
return [ this.namespace, key ].join('/');
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
},
|
||||
registerSetting: function(id, key) {
|
||||
assert(id, 'registerSetting -- invalid id: ' + id + ' for key:' + key);
|
||||
this.id2Setting[id] = key;
|
||||
if (!(key in this.Setting2id)) {
|
||||
this.Setting2id[key] = id;
|
||||
} else {
|
||||
key = null;
|
||||
}
|
||||
debugPrint('JQuerySettings.registerSetting -- registered: ' + JSON.stringify({ id: id, key: key }));
|
||||
},
|
||||
registerNode: function(node) {
|
||||
var element = $(node),
|
||||
target = element.data('for') || element.attr('for') || element.prop('id');
|
||||
assert(target, 'registerNode could determine settings target: ' + node.outerHTML);
|
||||
if (!node.id) {
|
||||
node.id = ['id', target.replace(/[^-\w]/g,'-'), JQuerySettings.idCounter++ ].join('-');
|
||||
}
|
||||
var key = node.dataset['key'] = this.resolve(target);
|
||||
this.registerSetting(node.id, key);
|
||||
|
||||
debugPrint('registerNode', node.id, target, key);
|
||||
// return this.observeNode(node);
|
||||
},
|
||||
// lookup the DOM id for a given Settings key
|
||||
getId: function(key, missingOk) {
|
||||
key = this.resolve(key);
|
||||
assert(missingOk || function assertion(){
|
||||
return typeof key === 'string';
|
||||
});
|
||||
if (key in this.Setting2id || missingOk) {
|
||||
return this.Setting2id[key];
|
||||
}
|
||||
log('WARNING: jquerySettings.getId: !Setting2id['+key+'] ' + this.Setting2id[key], key in this.Setting2id);
|
||||
},
|
||||
getAllNodes: function() {
|
||||
return Object.keys(this.Setting2id)
|
||||
.map(function(key) {
|
||||
return this.findNodeByKey(key);
|
||||
}.bind(this))
|
||||
.filter(function(node) {
|
||||
return node.type !== 'placeholder';
|
||||
}).filter(Boolean);
|
||||
},
|
||||
// lookup the Settings key for a given DOM id
|
||||
getKey: function(id, missingOk) {
|
||||
if ((id in this.id2Setting) || missingOk) {
|
||||
return this.id2Setting[id];
|
||||
}
|
||||
log('WARNING: jquerySettings.getKey: !id2Setting['+id+']');
|
||||
},
|
||||
// lookup the DOM node for a given Settings key
|
||||
findNodeByKey: function(key, missingOk) {
|
||||
key = this.resolve(key);
|
||||
var id = this.getId(key, missingOk);
|
||||
var node = typeof id === 'object' ? id : document.getElementById(id);
|
||||
if (node || missingOk) {
|
||||
return node;
|
||||
}
|
||||
log('WARNING: jquerySettings.findNodeByKey -- node not found:', 'key=='+key, 'id=='+id);
|
||||
},
|
||||
getValue: function(key, defaultValue) {
|
||||
return this.getNodeValue(this.findNodeByKey(key));
|
||||
},
|
||||
setValue: function(key, value, origin) {
|
||||
return this.setNodeValue(this.findNodeByKey(key), value, origin || 'setValue');
|
||||
},
|
||||
getNodeValue: function(node) {
|
||||
assert(node && typeof node === 'object', 'getNodeValue expects a DOM node');
|
||||
node = node.jquery ? node.get(0) : node;
|
||||
if (node.type === 'placeholder') {
|
||||
return node.value;
|
||||
}
|
||||
assert(node.dataset.hifiType);
|
||||
return $(node)[node.dataset.hifiType]('instance').value();
|
||||
},
|
||||
setNodeValue: function(node, newValue) {
|
||||
assert(node, 'JQuerySettings::setNodeValue -- invalid node:' + node);
|
||||
node = node.jquery ? node[0] : node;
|
||||
if (node.type === 'placeholder') {
|
||||
return node.value = newValue;
|
||||
}
|
||||
var hifiType = assert(node.dataset.hifiType);
|
||||
return $(node)[hifiType]('instance').value(newValue);
|
||||
},
|
||||
};
|
||||
})(this);
|
|
@ -0,0 +1,773 @@
|
|||
// movement-utils.js -- helper classes for managing related Controller.*Event and input API bindings
|
||||
|
||||
/* eslint-disable comma-dangle, no-empty */
|
||||
/* global require: true, DriveKeys, console, __filename, __dirname */
|
||||
/* eslint-env commonjs */
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
version: '0.0.2c',
|
||||
|
||||
CameraControls: CameraControls,
|
||||
MovementEventMapper: MovementEventMapper,
|
||||
MovementMapping: MovementMapping,
|
||||
VelocityTracker: VelocityTracker,
|
||||
VirtualDriveKeys: VirtualDriveKeys,
|
||||
|
||||
applyEasing: applyEasing,
|
||||
calculateThrust: calculateThrust,
|
||||
vec3damp: vec3damp,
|
||||
vec3eclamp: vec3eclamp,
|
||||
|
||||
DriveModes: {
|
||||
POSITION: 'position', // ~ MyAvatar.position
|
||||
MOTOR: 'motor', // ~ MyAvatar.motorVelocity
|
||||
THRUST: 'thrust', // ~ MyAvatar.setThrust
|
||||
},
|
||||
};
|
||||
|
||||
var MAPPING_TEMPLATE = require('./movement-utils.mapping.json');
|
||||
var WANT_DEBUG = false;
|
||||
|
||||
function log() {
|
||||
// eslint-disable-next-line no-console
|
||||
(typeof Script === 'object' ? print : console.log)('movement-utils | ' + [].slice.call(arguments).join(' '));
|
||||
}
|
||||
|
||||
var debugPrint = function() {};
|
||||
|
||||
log(module.exports.version);
|
||||
|
||||
var _utils = require('./_utils.js'),
|
||||
assert = _utils.assert;
|
||||
|
||||
if (WANT_DEBUG) {
|
||||
require = _utils.makeDebugRequire(__dirname);
|
||||
_utils = require('./_utils.js'); // re-require in debug mode
|
||||
debugPrint = log;
|
||||
}
|
||||
|
||||
Object.assign = Object.assign || _utils.assign;
|
||||
|
||||
var enumMeta = require('./EnumMeta.js');
|
||||
assert(enumMeta.version >= '0.0.1', 'enumMeta >= 0.0.1 expected but got: ' + enumMeta.version);
|
||||
|
||||
Object.assign(MovementEventMapper, {
|
||||
CAPTURE_DRIVE_KEYS: 'drive-keys',
|
||||
CAPTURE_ACTION_EVENTS: 'action-events',
|
||||
});
|
||||
|
||||
function MovementEventMapper(options) {
|
||||
assert('namespace' in options, '.namespace expected ' + Object.keys(options) );
|
||||
this.namespace = options.namespace;
|
||||
this.enabled = false;
|
||||
|
||||
this.options = Object.assign({
|
||||
namespace: this.namespace,
|
||||
captureMode: MovementEventMapper.CAPTURE_ACTION_EVENTS,
|
||||
excludeNames: null,
|
||||
mouseSmooth: true,
|
||||
keyboardMultiplier: 1.0,
|
||||
mouseMultiplier: 1.0,
|
||||
eventFilter: null,
|
||||
controllerMapping: MAPPING_TEMPLATE,
|
||||
}, options);
|
||||
|
||||
this.isShifted = false;
|
||||
this.isGrounded = false;
|
||||
this.isRightMouseButton = false;
|
||||
this.rightMouseButtonReleased = undefined;
|
||||
|
||||
this.inputMapping = new MovementMapping(this.options);
|
||||
this.inputMapping.virtualActionEvent.connect(this, 'onVirtualActionEvent');
|
||||
}
|
||||
MovementEventMapper.prototype = {
|
||||
constructor: MovementEventMapper,
|
||||
defaultEventFilter: function(from, event) {
|
||||
return event.actionValue;
|
||||
},
|
||||
getState: function(options) {
|
||||
var state = this.states ? this.states.getDriveKeys(options) : {};
|
||||
|
||||
state.enabled = this.enabled;
|
||||
|
||||
state.mouseSmooth = this.options.mouseSmooth;
|
||||
state.captureMode = this.options.captureMode;
|
||||
state.mouseMultiplier = this.options.mouseMultiplier;
|
||||
state.keyboardMultiplier = this.options.keyboardMultiplier;
|
||||
|
||||
state.isGrounded = this.isGrounded;
|
||||
state.isShifted = this.isShifted;
|
||||
state.isRightMouseButton = this.isRightMouseButton;
|
||||
state.rightMouseButtonReleased = this.rightMouseButtonReleased;
|
||||
|
||||
return state;
|
||||
},
|
||||
updateOptions: function(options) {
|
||||
return _updateOptions(this.options, options || {}, this.constructor.name);
|
||||
},
|
||||
applyOptions: function(options, applyNow) {
|
||||
if (this.updateOptions(options) && applyNow) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
reset: function() {
|
||||
if (this.enabled) {
|
||||
this.disable();
|
||||
this.enable();
|
||||
}
|
||||
},
|
||||
disable: function() {
|
||||
this.inputMapping.disable();
|
||||
this.bindEvents(false);
|
||||
this.enabled = false;
|
||||
},
|
||||
enable: function() {
|
||||
if (!this.enabled) {
|
||||
this.enabled = true;
|
||||
this.states = new VirtualDriveKeys({
|
||||
eventFilter: this.options.eventFilter && _utils.bind(this, this.options.eventFilter)
|
||||
});
|
||||
this.bindEvents(true);
|
||||
this.inputMapping.updateOptions(this.options);
|
||||
this.inputMapping.enable();
|
||||
}
|
||||
},
|
||||
bindEvents: function bindEvents(capture) {
|
||||
var captureMode = this.options.captureMode;
|
||||
assert(function assertion() {
|
||||
return captureMode === MovementEventMapper.CAPTURE_ACTION_EVENTS ||
|
||||
captureMode === MovementEventMapper.CAPTURE_DRIVE_KEYS;
|
||||
});
|
||||
log('bindEvents....', capture, this.options.captureMode);
|
||||
var exclude = Array.isArray(this.options.excludeNames) && this.options.excludeNames;
|
||||
|
||||
var tmp;
|
||||
if (!capture || this.options.captureMode === MovementEventMapper.CAPTURE_ACTION_EVENTS) {
|
||||
tmp = capture ? 'captureActionEvents' : 'releaseActionEvents';
|
||||
log('bindEvents -- ', tmp.toUpperCase());
|
||||
Controller[tmp]();
|
||||
}
|
||||
if (!capture || this.options.captureMode === MovementEventMapper.CAPTURE_DRIVE_KEYS) {
|
||||
tmp = capture ? 'disableDriveKey' : 'enableDriveKey';
|
||||
log('bindEvents -- ', tmp.toUpperCase());
|
||||
for (var p in DriveKeys) {
|
||||
if (capture && (exclude && ~exclude.indexOf(p))) {
|
||||
log(tmp.toUpperCase(), 'excluding DriveKey===' + p);
|
||||
} else {
|
||||
MyAvatar[tmp](DriveKeys[p]);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
Controller.actionEvent[capture ? 'connect' : 'disconnect'](this, 'onActionEvent');
|
||||
} catch (e) { }
|
||||
|
||||
if (!capture || !/person/i.test(Camera.mode)) {
|
||||
Controller[capture ? 'captureWheelEvents' : 'releaseWheelEvents']();
|
||||
try {
|
||||
Controller.wheelEvent[capture ? 'connect' : 'disconnect'](this, 'onWheelEvent');
|
||||
} catch (e) { /* eslint-disable-line empty-block */ }
|
||||
}
|
||||
},
|
||||
onWheelEvent: function onWheelEvent(event) {
|
||||
var actionID = enumMeta.ACTION_TRANSLATE_CAMERA_Z,
|
||||
actionValue = -event.delta;
|
||||
return this.onActionEvent(actionID, actionValue, event);
|
||||
},
|
||||
onActionEvent: function(actionID, actionValue, extra) {
|
||||
var actionName = enumMeta.Controller.ActionNames[actionID],
|
||||
driveKeyName = enumMeta.getDriveKeyNameFromActionName(actionName),
|
||||
prefix = (actionValue > 0 ? '+' : actionValue < 0 ? '-' : ' ');
|
||||
|
||||
var event = {
|
||||
id: prefix + actionName,
|
||||
actionName: actionName,
|
||||
driveKey: DriveKeys[driveKeyName],
|
||||
driveKeyName: driveKeyName,
|
||||
actionValue: actionValue,
|
||||
extra: extra
|
||||
};
|
||||
// debugPrint('onActionEvent', actionID, actionName, driveKeyName);
|
||||
this.states.handleActionEvent('Actions.' + actionName, event);
|
||||
},
|
||||
onVirtualActionEvent: function(from, event) {
|
||||
if (from === 'Application.Grounded') {
|
||||
this.isGrounded = !!event.applicationValue;
|
||||
} else if (from === 'Keyboard.Shift') {
|
||||
this.isShifted = !!event.value;
|
||||
} else if (from === 'Keyboard.RightMouseButton') {
|
||||
this.isRightMouseButton = !!event.value;
|
||||
this.rightMouseButtonReleased = !event.value ? new Date : undefined;
|
||||
}
|
||||
this.states.handleActionEvent(from, event);
|
||||
}
|
||||
}; // MovementEventMapper.prototype
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// helper JS class to track drive keys -> translation / rotation influences
|
||||
function VirtualDriveKeys(options) {
|
||||
options = options || {};
|
||||
Object.defineProperties(this, {
|
||||
$pendingReset: { value: {} },
|
||||
$eventFilter: { value: options.eventFilter },
|
||||
$valueUpdated: { value: _utils.signal(function valueUpdated(action, newValue, oldValue){}) }
|
||||
});
|
||||
}
|
||||
VirtualDriveKeys.prototype = {
|
||||
constructor: VirtualDriveKeys,
|
||||
update: function update(dt) {
|
||||
Object.keys(this.$pendingReset).forEach(_utils.bind(this, function(i) {
|
||||
var event = this.$pendingReset[i].event;
|
||||
(event.driveKey in this) && this.setValue(event, 0);
|
||||
}));
|
||||
},
|
||||
getValue: function(driveKey, defaultValue) {
|
||||
return driveKey in this ? this[driveKey] : defaultValue;
|
||||
},
|
||||
_defaultFilter: function(from, event) {
|
||||
return event.actionValue;
|
||||
},
|
||||
handleActionEvent: function(from, event) {
|
||||
var value = this.$eventFilter ? this.$eventFilter(from, event, this._defaultFilter) : event.actionValue;
|
||||
return event.driveKeyName && this.setValue(event, value);
|
||||
},
|
||||
setValue: function(event, value) {
|
||||
var driveKeyName = event.driveKeyName,
|
||||
driveKey = DriveKeys[driveKeyName],
|
||||
id = event.id,
|
||||
previous = this[driveKey],
|
||||
autoReset = (driveKeyName === 'ZOOM');
|
||||
|
||||
this[driveKey] = value;
|
||||
|
||||
if (previous !== value) {
|
||||
this.$valueUpdated(event, value, previous);
|
||||
}
|
||||
if (value === 0.0) {
|
||||
delete this.$pendingReset[id];
|
||||
} else if (autoReset) {
|
||||
this.$pendingReset[id] = { event: event, value: value };
|
||||
}
|
||||
},
|
||||
reset: function() {
|
||||
Object.keys(this).forEach(_utils.bind(this, function(p) {
|
||||
this[p] = 0.0;
|
||||
}));
|
||||
Object.keys(this.$pendingReset).forEach(_utils.bind(this, function(p) {
|
||||
delete this.$pendingReset[p];
|
||||
}));
|
||||
},
|
||||
toJSON: function() {
|
||||
var obj = {};
|
||||
for (var key in this) {
|
||||
if (enumMeta.DriveKeyNames[key]) {
|
||||
obj[enumMeta.DriveKeyNames[key]] = this[key];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
getDriveKeys: function(options) {
|
||||
options = options || {};
|
||||
try {
|
||||
return {
|
||||
translation: {
|
||||
x: this.getValue(DriveKeys.TRANSLATE_X) || 0,
|
||||
y: this.getValue(DriveKeys.TRANSLATE_Y) || 0,
|
||||
z: this.getValue(DriveKeys.TRANSLATE_Z) || 0
|
||||
},
|
||||
rotation: {
|
||||
x: this.getValue(DriveKeys.PITCH) || 0,
|
||||
y: this.getValue(DriveKeys.YAW) || 0,
|
||||
z: 'ROLL' in DriveKeys && this.getValue(DriveKeys.ROLL) || 0
|
||||
},
|
||||
zoom: Vec3.multiply(this.getValue(DriveKeys.ZOOM) || 0, Vec3.ONE)
|
||||
};
|
||||
} finally {
|
||||
options.update && this.update(options.update);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// MovementMapping
|
||||
|
||||
function MovementMapping(options) {
|
||||
options = options || {};
|
||||
assert('namespace' in options && 'controllerMapping' in options);
|
||||
this.namespace = options.namespace;
|
||||
this.enabled = false;
|
||||
this.options = {
|
||||
keyboardMultiplier: 1.0,
|
||||
mouseMultiplier: 1.0,
|
||||
mouseSmooth: true,
|
||||
captureMode: MovementEventMapper.CAPTURE_ACTION_EVENTS,
|
||||
excludeNames: null,
|
||||
controllerMapping: MAPPING_TEMPLATE,
|
||||
};
|
||||
this.updateOptions(options);
|
||||
this.virtualActionEvent = _utils.signal(function virtualActionEvent(from, event) {});
|
||||
}
|
||||
MovementMapping.prototype = {
|
||||
constructor: MovementMapping,
|
||||
enable: function() {
|
||||
this.enabled = true;
|
||||
if (this.mapping) {
|
||||
this.mapping.disable();
|
||||
}
|
||||
this.mapping = this._createMapping();
|
||||
log('ENABLE CONTROLLER MAPPING', this.mapping.name);
|
||||
this.mapping.enable();
|
||||
},
|
||||
disable: function() {
|
||||
this.enabled = false;
|
||||
if (this.mapping) {
|
||||
log('DISABLE CONTROLLER MAPPING', this.mapping.name);
|
||||
this.mapping.disable();
|
||||
}
|
||||
},
|
||||
reset: function() {
|
||||
var enabled = this.enabled;
|
||||
enabled && this.disable();
|
||||
this.mapping = this._createMapping();
|
||||
enabled && this.enable();
|
||||
},
|
||||
updateOptions: function(options) {
|
||||
return _updateOptions(this.options, options || {}, this.constructor.name);
|
||||
},
|
||||
applyOptions: function(options, applyNow) {
|
||||
if (this.updateOptions(options) && applyNow) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
onShiftKey: function onShiftKey(value, key) {
|
||||
var event = {
|
||||
type: value ? 'keypress' : 'keyrelease',
|
||||
keyboardKey: key,
|
||||
keyboardText: 'SHIFT',
|
||||
keyboardValue: value,
|
||||
actionName: 'Shift',
|
||||
actionValue: !!value,
|
||||
value: !!value,
|
||||
at: +new Date
|
||||
};
|
||||
this.virtualActionEvent('Keyboard.Shift', event);
|
||||
},
|
||||
onRightMouseButton: function onRightMouseButton(value, key) {
|
||||
var event = {
|
||||
type: value ? 'mousepress' : 'mouserelease',
|
||||
keyboardKey: key,
|
||||
keyboardValue: value,
|
||||
actionName: 'RightMouseButton',
|
||||
actionValue: !!value,
|
||||
value: !!value,
|
||||
at: +new Date
|
||||
};
|
||||
this.virtualActionEvent('Keyboard.RightMouseButton', event);
|
||||
},
|
||||
onApplicationEvent: function _onApplicationEvent(key, name, value) {
|
||||
var event = {
|
||||
type: 'application',
|
||||
actionName: 'Application.' + name,
|
||||
applicationKey: key,
|
||||
applicationName: name,
|
||||
applicationValue: value,
|
||||
actionValue: !!value,
|
||||
value: !!value
|
||||
};
|
||||
this.virtualActionEvent('Application.' + name, event);
|
||||
},
|
||||
_createMapping: function() {
|
||||
this._mapping = this._getTemplate();
|
||||
var mappingJSON = JSON.stringify(this._mapping, 0, 2);
|
||||
var mapping = Controller.parseMapping(mappingJSON);
|
||||
debugPrint(mappingJSON);
|
||||
mapping.name = mapping.name || this._mapping.name;
|
||||
|
||||
mapping.from(Controller.Hardware.Keyboard.Shift).peek().to(_utils.bind(this, 'onShiftKey'));
|
||||
mapping.from(Controller.Hardware.Keyboard.RightMouseButton).peek().to(_utils.bind(this, 'onRightMouseButton'));
|
||||
|
||||
var boundApplicationHandler = _utils.bind(this, 'onApplicationEvent');
|
||||
Object.keys(Controller.Hardware.Application).forEach(function(name) {
|
||||
var key = Controller.Hardware.Application[name];
|
||||
debugPrint('observing Controller.Hardware.Application.'+ name, key);
|
||||
mapping.from(key).to(function(value) {
|
||||
boundApplicationHandler(key, name, value);
|
||||
});
|
||||
});
|
||||
|
||||
return mapping;
|
||||
},
|
||||
_getTemplate: function() {
|
||||
assert(this.options.controllerMapping, 'MovementMapping._getTemplate -- !this.options.controllerMapping');
|
||||
var template = JSON.parse(JSON.stringify(this.options.controllerMapping)); // make a local copy
|
||||
template.name = this.namespace;
|
||||
template.channels = template.channels.filter(function(item) {
|
||||
// ignore any "JSON comment" or other bindings without a from spec
|
||||
return item.from && item.from.makeAxis;
|
||||
});
|
||||
var exclude = Array.isArray(this.options.excludeNames) ? this.options.excludeNames : [];
|
||||
if (!this.options.mouseSmooth) {
|
||||
exclude.push('Keyboard.RightMouseButton');
|
||||
}
|
||||
|
||||
log('EXCLUSIONS:' + exclude);
|
||||
|
||||
template.channels = template.channels.filter(_utils.bind(this, function(item, i) {
|
||||
debugPrint('channel['+i+']', item.from && item.from.makeAxis, item.to, JSON.stringify(item.filters) || '');
|
||||
// var hasFilters = Array.isArray(item.filters) && !item.filters[1];
|
||||
item.filters = Array.isArray(item.filters) ? item.filters :
|
||||
typeof item.filters === 'string' ? [ { type: item.filters }] : [ item.filters ];
|
||||
|
||||
if (/Mouse/.test(item.from && item.from.makeAxis)) {
|
||||
item.filters.push({ type: 'scale', scale: this.options.mouseMultiplier });
|
||||
log('applied mouse multiplier:', item.from.makeAxis, item.when, item.to, this.options.mouseMultiplier);
|
||||
} else if (/Keyboard/.test(item.from && item.from.makeAxis)) {
|
||||
item.filters.push({ type: 'scale', scale: this.options.keyboardMultiplier });
|
||||
log('applied keyboard multiplier:', item.from.makeAxis, item.when, item.to, this.options.keyboardMultiplier);
|
||||
}
|
||||
item.filters = item.filters.filter(Boolean);
|
||||
if (~exclude.indexOf(item.to)) {
|
||||
log('EXCLUDING item.to === ' + item.to);
|
||||
return false;
|
||||
}
|
||||
var when = Array.isArray(item.when) ? item.when : [item.when];
|
||||
for (var j=0; j < when.length; j++) {
|
||||
if (~exclude.indexOf(when[j])) {
|
||||
log('EXCLUDING item.when contains ' + when[j]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function shouldInclude(p, i) {
|
||||
if (~exclude.indexOf(p)) {
|
||||
log('EXCLUDING from.makeAxis[][' + i + '] === ' + p);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.from && Array.isArray(item.from.makeAxis)) {
|
||||
var makeAxis = item.from.makeAxis;
|
||||
item.from.makeAxis = makeAxis.map(function(axis) {
|
||||
if (Array.isArray(axis)) {
|
||||
return axis.filter(shouldInclude);
|
||||
} else {
|
||||
return shouldInclude(axis, -1) && axis;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
debugPrint(JSON.stringify(template,0,2));
|
||||
return template;
|
||||
}
|
||||
}; // MovementMapping.prototype
|
||||
|
||||
// update target properties from source, but iff the property already exists in target
|
||||
function _updateOptions(target, source, debugName) {
|
||||
debugName = debugName || '_updateOptions';
|
||||
var changed = 0;
|
||||
if (!source || typeof source !== 'object') {
|
||||
return changed;
|
||||
}
|
||||
for (var p in target) {
|
||||
if (p in source && target[p] !== source[p]) {
|
||||
log(debugName, 'updating source.'+p, target[p] + ' -> ' + source[p]);
|
||||
target[p] = source[p];
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
for (p in source) {
|
||||
(!(p in target)) && log(debugName, 'warning: ignoring unknown option:', p, (source[p] +'').substr(0, 40)+'...');
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
function calculateThrust(maxVelocity, targetVelocity, previousThrust) {
|
||||
var THRUST_FALLOFF = 0.1; // round to ZERO if component is below this threshold
|
||||
// Note: MyAvatar.setThrust might need an update to account for the recent avatar density changes...
|
||||
// For now, this discovered scaling factor seems to accomodate a similar easing effect to the other movement models.
|
||||
var magicScalingFactor = 12.0 * (maxVelocity + 120) / 16 - Math.sqrt( maxVelocity / 8 );
|
||||
|
||||
var targetThrust = Vec3.multiply(magicScalingFactor, targetVelocity);
|
||||
targetThrust = vec3eclamp(targetThrust, THRUST_FALLOFF, maxVelocity);
|
||||
if (Vec3.length(MyAvatar.velocity) > maxVelocity) {
|
||||
targetThrust = Vec3.multiply(0.5, targetThrust);
|
||||
}
|
||||
return targetThrust;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// clamp components and magnitude to maxVelocity, rounding to Vec3.ZERO if below epsilon
|
||||
function vec3eclamp(velocity, epsilon, maxVelocity) {
|
||||
velocity = {
|
||||
x: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.x)),
|
||||
y: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.y)),
|
||||
z: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.z))
|
||||
};
|
||||
|
||||
if (Math.abs(velocity.x) < epsilon) {
|
||||
velocity.x = 0;
|
||||
}
|
||||
if (Math.abs(velocity.y) < epsilon) {
|
||||
velocity.y = 0;
|
||||
}
|
||||
if (Math.abs(velocity.z) < epsilon) {
|
||||
velocity.z = 0;
|
||||
}
|
||||
|
||||
var length = Vec3.length(velocity);
|
||||
if (length > maxVelocity) {
|
||||
velocity = Vec3.multiply(maxVelocity, Vec3.normalize(velocity));
|
||||
} else if (length < epsilon) {
|
||||
velocity = Vec3.ZERO;
|
||||
}
|
||||
return velocity;
|
||||
}
|
||||
|
||||
function vec3damp(active, positiveEffect, negativeEffect) {
|
||||
// If force isn't being applied in a direction, incorporate negative effect (drag);
|
||||
negativeEffect = {
|
||||
x: active.x ? 0 : negativeEffect.x,
|
||||
y: active.y ? 0 : negativeEffect.y,
|
||||
z: active.z ? 0 : negativeEffect.z,
|
||||
};
|
||||
return Vec3.subtract(Vec3.sum(active, positiveEffect), negativeEffect);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
function VelocityTracker(defaultValues) {
|
||||
Object.defineProperty(this, 'defaultValues', { configurable: true, value: defaultValues });
|
||||
}
|
||||
VelocityTracker.prototype = {
|
||||
constructor: VelocityTracker,
|
||||
reset: function() {
|
||||
Object.assign(this, this.defaultValues);
|
||||
},
|
||||
integrate: function(targetState, currentVelocities, drag, settings) {
|
||||
var args = [].slice.call(arguments);
|
||||
this._applyIntegration('translation', args);
|
||||
this._applyIntegration('rotation', args);
|
||||
this._applyIntegration('zoom', args);
|
||||
},
|
||||
_applyIntegration: function(component, args) {
|
||||
return this._integrate.apply(this, [component].concat(args));
|
||||
},
|
||||
_integrate: function(component, targetState, currentVelocities, drag, settings) {
|
||||
assert(targetState[component], component + ' not found in targetState (which has: ' + Object.keys(targetState) + ')');
|
||||
var result = vec3damp(
|
||||
targetState[component],
|
||||
currentVelocities[component],
|
||||
drag[component]
|
||||
);
|
||||
var maxVelocity = settings[component].maxVelocity;
|
||||
return this[component] = vec3eclamp(result, settings.epsilon, maxVelocity);
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ----------------------------------------------------------------------------
|
||||
Object.assign(CameraControls, {
|
||||
SCRIPT_UPDATE: 'update',
|
||||
ANIMATION_FRAME: 'requestAnimationFrame', // emulated
|
||||
NEXT_TICK: 'nextTick', // emulated
|
||||
SET_IMMEDIATE: 'setImmediate', // emulated
|
||||
//WORKER_THREAD: 'workerThread',
|
||||
});
|
||||
|
||||
function CameraControls(options) {
|
||||
options = options || {};
|
||||
assert('update' in options && 'threadMode' in options);
|
||||
this.updateObject = typeof options.update === 'function' ? options : options.update;
|
||||
assert(typeof this.updateObject.update === 'function',
|
||||
'construction options expected either { update: function(){}... } object or a function(){}');
|
||||
this.update = _utils.bind(this.updateObject, 'update');
|
||||
this.threadMode = options.threadMode;
|
||||
this.fps = options.fps || 60;
|
||||
this.getRuntimeSeconds = options.getRuntimeSeconds || function() {
|
||||
return +new Date / 1000.0;
|
||||
};
|
||||
this.backupOptions = _utils.DeferredUpdater.createGroup({
|
||||
MyAvatar: MyAvatar,
|
||||
Camera: Camera,
|
||||
Reticle: Reticle,
|
||||
});
|
||||
|
||||
this.enabled = false;
|
||||
this.enabledChanged = _utils.signal(function enabledChanged(enabled){});
|
||||
this.modeChanged = _utils.signal(function modeChanged(mode, oldMode){});
|
||||
}
|
||||
CameraControls.prototype = {
|
||||
constructor: CameraControls,
|
||||
$animate: null,
|
||||
$start: function() {
|
||||
if (this.$animate) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastTime;
|
||||
switch (this.threadMode) {
|
||||
case CameraControls.SCRIPT_UPDATE: {
|
||||
this.$animate = this.update;
|
||||
Script.update.connect(this, '$animate');
|
||||
this.$animate.disconnect = _utils.bind(this, function() {
|
||||
Script.update.disconnect(this, '$animate');
|
||||
});
|
||||
} break;
|
||||
|
||||
case CameraControls.ANIMATION_FRAME: {
|
||||
this.requestAnimationFrame = _utils.createAnimationStepper({
|
||||
getRuntimeSeconds: this.getRuntimeSeconds,
|
||||
fps: this.fps
|
||||
});
|
||||
this.$animate = _utils.bind(this, function(dt) {
|
||||
this.update(dt);
|
||||
this.requestAnimationFrame(this.$animate);
|
||||
});
|
||||
this.$animate.disconnect = _utils.bind(this.requestAnimationFrame, 'reset');
|
||||
this.requestAnimationFrame(this.$animate);
|
||||
} break;
|
||||
|
||||
case CameraControls.SET_IMMEDIATE: {
|
||||
// emulate process.setImmediate (attempt to execute at start of next update frame, sans Script.update throttling)
|
||||
lastTime = this.getRuntimeSeconds();
|
||||
this.$animate = Script.setInterval(_utils.bind(this, function() {
|
||||
this.update(this.getRuntimeSeconds(lastTime));
|
||||
lastTime = this.getRuntimeSeconds();
|
||||
}), 5);
|
||||
this.$animate.disconnect = function() {
|
||||
Script.clearInterval(this);
|
||||
};
|
||||
} break;
|
||||
|
||||
case CameraControls.NEXT_TICK: {
|
||||
// emulate process.nextTick (attempt to queue at the very next opportunity beyond current scope)
|
||||
lastTime = this.getRuntimeSeconds();
|
||||
this.$animate = _utils.bind(this, function() {
|
||||
this.$animate.timeout = 0;
|
||||
if (this.$animate.quit) {
|
||||
return;
|
||||
}
|
||||
this.update(this.getRuntimeSeconds(lastTime));
|
||||
lastTime = this.getRuntimeSeconds();
|
||||
this.$animate.timeout = Script.setTimeout(this.$animate, 0);
|
||||
});
|
||||
this.$animate.quit = false;
|
||||
this.$animate.disconnect = function() {
|
||||
this.timeout && Script.clearTimeout(this.timeout);
|
||||
this.timeout = 0;
|
||||
this.quit = true;
|
||||
};
|
||||
this.$animate();
|
||||
} break;
|
||||
|
||||
default: throw new Error('unknown threadMode: ' + this.threadMode);
|
||||
}
|
||||
log(
|
||||
'...$started update thread', '(threadMode: ' + this.threadMode + ')',
|
||||
this.threadMode === CameraControls.ANIMATION_FRAME && this.fps
|
||||
);
|
||||
},
|
||||
$stop: function() {
|
||||
if (!this.$animate) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.$animate.disconnect();
|
||||
} catch (e) {
|
||||
log('$animate.disconnect error: ' + e, '(threadMode: ' + this.threadMode +')');
|
||||
}
|
||||
this.$animate = null;
|
||||
log('...$stopped updated thread', '(threadMode: ' + this.threadMode +')');
|
||||
},
|
||||
onModeUpdated: function onModeUpdated(mode, oldMode) {
|
||||
oldMode = oldMode || this.previousMode;
|
||||
this.previousMode = mode;
|
||||
log('onModeUpdated', oldMode + '->' + mode);
|
||||
// user changed modes, so leave the current mode intact later when restoring backup values
|
||||
delete this.backupOptions.Camera.$setModeString;
|
||||
if (/person/.test(oldMode) && /person/.test(mode)) {
|
||||
return; // disregard first -> third and third ->first transitions
|
||||
}
|
||||
this.modeChanged(mode, oldMode);
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
if (this.enabled) {
|
||||
this.disable();
|
||||
this.enable();
|
||||
}
|
||||
},
|
||||
setEnabled: function setEnabled(enabled) {
|
||||
if (!this.enabled && enabled) {
|
||||
this.enable();
|
||||
} else if (this.enabled && !enabled) {
|
||||
this.disable();
|
||||
}
|
||||
},
|
||||
enable: function enable() {
|
||||
if (this.enabled) {
|
||||
throw new Error('CameraControls.enable -- already enabled..');
|
||||
}
|
||||
log('ENABLE enableCameraMove', this.threadMode);
|
||||
|
||||
this._backup();
|
||||
|
||||
this.previousMode = Camera.mode;
|
||||
Camera.modeUpdated.connect(this, 'onModeUpdated');
|
||||
|
||||
this.$start();
|
||||
|
||||
this.enabledChanged(this.enabled = true);
|
||||
},
|
||||
disable: function disable() {
|
||||
log("DISABLE CameraControls");
|
||||
try {
|
||||
Camera.modeUpdated.disconnect(this, 'onModeUpdated');
|
||||
} catch (e) {
|
||||
debugPrint(e);
|
||||
}
|
||||
this.$stop();
|
||||
|
||||
this._restore();
|
||||
|
||||
if (this.enabled !== false) {
|
||||
this.enabledChanged(this.enabled = false);
|
||||
}
|
||||
},
|
||||
_restore: function() {
|
||||
var submitted = this.backupOptions.submit();
|
||||
log('restored previous values: ' + JSON.stringify(submitted,0,2));
|
||||
return submitted;
|
||||
},
|
||||
_backup: function() {
|
||||
this.backupOptions.reset();
|
||||
Object.assign(this.backupOptions.Reticle, {
|
||||
scale: Reticle.scale,
|
||||
});
|
||||
Object.assign(this.backupOptions.Camera, {
|
||||
$setModeString: Camera.mode,
|
||||
});
|
||||
Object.assign(this.backupOptions.MyAvatar, {
|
||||
motorTimescale: MyAvatar.motorTimescale,
|
||||
motorReferenceFrame: MyAvatar.motorReferenceFrame,
|
||||
motorVelocity: Vec3.ZERO,
|
||||
velocity: Vec3.ZERO,
|
||||
angularVelocity: Vec3.ZERO,
|
||||
});
|
||||
},
|
||||
}; // CameraControls
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
function applyEasing(deltaTime, direction, settings, state, scaling) {
|
||||
var obj = {};
|
||||
for (var p in scaling) {
|
||||
var group = settings[p], // translation | rotation | zoom
|
||||
easeConst = group[direction], // easeIn | easeOut
|
||||
scale = scaling[p],
|
||||
stateVector = state[p];
|
||||
obj[p] = Vec3.multiply(easeConst * scale * deltaTime, stateVector);
|
||||
}
|
||||
return obj;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"name": "app-camera-move",
|
||||
"channels": [
|
||||
|
||||
{ "comment": "------------------ Actions.TranslateX -------------------" },
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]},
|
||||
"when": "Keyboard.Shift",
|
||||
"to": "Actions.TranslateX"
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.A","Keyboard.TouchpadLeft"],["Keyboard.D","Keyboard.TouchpadRight"]]},
|
||||
"when": "Keyboard.Shift",
|
||||
"to": "Actions.TranslateX"
|
||||
},
|
||||
|
||||
{ "comment": "------------------ Actions.TranslateY -------------------" },
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.C","Keyboard.PgDown"],["Keyboard.E","Keyboard.PgUp"]]},
|
||||
"when": "!Keyboard.Shift",
|
||||
"to": "Actions.TranslateY"
|
||||
},
|
||||
|
||||
{ "comment": "------------------ Actions.TranslateZ -------------------" },
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.W"],["Keyboard.S"]]},
|
||||
"when": "!Keyboard.Shift",
|
||||
"to": "Actions.TranslateZ"
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.Up"],["Keyboard.Down"]]},
|
||||
"when": "!Keyboard.Shift",
|
||||
"to": "Actions.TranslateZ"
|
||||
},
|
||||
|
||||
{ "comment": "------------------ Actions.Yaw -------------------" },
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.A","Keyboard.TouchpadLeft"],["Keyboard.D","Keyboard.TouchpadRight"]]},
|
||||
"when": "!Keyboard.Shift",
|
||||
"to": "Actions.Yaw",
|
||||
"filters": ["invert"]
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.MouseMoveLeft"],["Keyboard.MouseMoveRight"]]},
|
||||
"when": "Keyboard.RightMouseButton",
|
||||
"to": "Actions.Yaw",
|
||||
"filters": ["invert"]
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]},
|
||||
"when": "!Keyboard.Shift",
|
||||
"to": "Actions.Yaw",
|
||||
"filters": ["invert"]
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]},
|
||||
"when": ["Application.SnapTurn", "!Keyboard.Shift"],
|
||||
"to": "Actions.StepYaw",
|
||||
"filters":
|
||||
[
|
||||
"invert",
|
||||
{ "type": "pulse", "interval": 0.5, "resetOnZero": true },
|
||||
{ "type": "scale", "scale": 22.5 }
|
||||
]
|
||||
},
|
||||
|
||||
{ "comment": "------------------ Actions.Pitch -------------------" },
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.W"],["Keyboard.S"]]},
|
||||
"when": "Keyboard.Shift",
|
||||
"to": "Actions.Pitch",
|
||||
"filters": ["invert"]
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.MouseMoveUp"],["Keyboard.MouseMoveDown"]]},
|
||||
"when": "Keyboard.RightMouseButton",
|
||||
"to": "Actions.Pitch",
|
||||
"filters": ["invert"]
|
||||
},
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.Up"],["Keyboard.Down"]]},
|
||||
"when": "Keyboard.Shift",
|
||||
"to": "Actions.Pitch",
|
||||
"filters": ["invert"]
|
||||
},
|
||||
|
||||
{ "comment": "------------------ Actions.BoomIn -------------------" },
|
||||
{
|
||||
"from": {"makeAxis": [["Keyboard.C","Keyboard.PgDown"],["Keyboard.E","Keyboard.PgUp"]]},
|
||||
"when": "Keyboard.Shift",
|
||||
"to": "Actions.BoomIn",
|
||||
"filters": [{"type": "scale","scale": 0.005}]
|
||||
},
|
||||
|
||||
{ "comment": "------------------ end -------------------" }
|
||||
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue