diff --git a/unpublishedScripts/marketplace/camera-move/_debug.js b/unpublishedScripts/marketplace/camera-move/_debug.js index 1b90acffcf..ecec259663 100644 --- a/unpublishedScripts/marketplace/camera-move/_debug.js +++ b/unpublishedScripts/marketplace/camera-move/_debug.js @@ -1,11 +1,28 @@ -_debug = { +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global EventBridge: true, PARAMS, signal, assert, log, debugPrint, + bridgedSettings, _utils, jquerySettings, */ +var _debug = { handleUncaughtException: function onerror(message, fileName, lineNumber, colNumber, err) { - var output = _utils.normalizeStackTrace(err || { message: message }); - console.error('window.onerror: ' + output, 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 (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 refreshing the client script refreshes dependencies too @@ -20,6 +37,7 @@ _debug = { }, // TESTING MOCK (allows the UI to be tested using a normal web browser, outside of Interface openEventBridgeMock: function openEventBridgeMock(onEventBridgeOpened) { + var updatedValues = openEventBridgeMock.updatedValues = {}; // emulate EventBridge's API EventBridge = { emitWebEvent: signal(function emitWebEvent(message){}), @@ -27,63 +45,77 @@ _debug = { }; EventBridge.emitWebEvent.connect(onEmitWebEvent); onEventBridgeOpened(EventBridge); - setTimeout(function() { - assert(!bridgedSettings.onUnhandledMessage); - bridgedSettings.onUnhandledMessage = function(msg) { - return true; - }; - // manually trigger bootstrapping responses - $('.slider .control').parent().css('visibility','visible'); - bridgedSettings.handleExtraParams({uuid: PARAMS.uuid, ns: PARAMS.ns, extraParams: { - mock: true, - appVersion: 'browsermock', - toggleKey: { text: 'SPACE', isShifted: true }, - } }); - bridgedSettings.setValue('ui-show-advanced-options', true); - if (/fps/.test(location.hash)) setTimeout(function() { $('#fps').each(function(){ this.scrollIntoView(); }); }, 100); - },1); + assert(!bridgedSettings.onUnhandledMessage); + bridgedSettings.onUnhandledMessage = function(msg) { + if (1||!msg || msg.method !== 'valueUpdated') { + log('bridgedSettings.onUnhandledMessage', msg); + } + return true; + }; + // manually trigger bootstrapping responses + 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: false, + hmd: false, + }, + } }); + 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))); } - var updatedValues = {}; // generate mock data in response to outgoing web events function onEmitWebEvent(message) { - try { var obj = JSON.parse(message); } catch(e) {} + try { + var obj = JSON.parse(message); + } catch (e) {} if (!obj) { // message isn't JSON or doesn't expect a reply so just log it and bail early log('consuming non-callback web event', message); return; } - switch(obj.method) { + switch (obj.method) { case 'valueUpdated': { + log('valueUpdated',obj.params); updatedValues[obj.params[0]] = obj.params[1]; - } break; + return; + } case 'Settings.getValue': { var key = obj.params[0]; var node = jquerySettings.findNodeByKey(key, true); - var type = node && (node.dataset.type || node.getAttribute('type')); - switch(type) { + // log('Settings.getValue.findNodeByKey', key, node); + var type = node && (node.dataset.type || node.type || node.attributes['type']); + switch (type) { case 'checkbox': { - obj.result = /tooltip/i.test(key) || PARAMS.tooltiptest ? true : Math.random() > .5; + obj.result = /tooltip/i.test(key) || PARAMS.tooltiptest ? true : Math.random() > 0.5; } break; case 'radio-group': { var radios = $(node).find('input[type=radio]').toArray(); - while(Math.random() < .9) { radios.push(radios.shift()); } + while (Math.random() < 0.9) { + radios.push(radios.shift()); + } obj.result = radios[0].value; } break; + case 'spinner': case 'number': { 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.getAttribute('type')] + ' @ ' + (node && node.id)); + 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; } - log('mock getValue data %c%s = %c%s', 'color:blue', + debugPrint('mock getValue data %c%s = %c%s', 'color:blue', JSON.stringify(key), 'color:green', JSON.stringify(obj.result)); } break; default: { diff --git a/unpublishedScripts/marketplace/camera-move/_json-persist.js b/unpublishedScripts/marketplace/camera-move/_json-persist.js new file mode 100644 index 0000000000..be253ff16b --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_json-persist.js @@ -0,0 +1,128 @@ +/* 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, '
')); + if (arguments[3] === '_overlayWebWindow') { + bridgedSettings.sendEvent({ + method: 'overlayWebWindow', + options: { + title: 'app-camera-move-export' + (title ? '::'+title : ''), + content: HTML, + }, + }); + } else { + // make the browser address bar less ugly by putting spaces and friedly name as a "URL footer" + 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('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 { + json = JSON.parse(json); + } catch (e) { + throw new Error('Could not parse pasted JSON: ' + e + '\n\n' + (json||'').replace(/").children(0).text(hash); + // LESS needs some help to recompile inline styles, so a fresh copy of the source nodes is swapped-in + var newNodes = lessManager.elements.clone().appendTo(document.body); + // less.modifyVars && less.modifyVars(true, globalVars); + // note: refresh(reload, modifyVars, clearFileCache) + less.refresh(false, globalVars).then(function(result) { + log('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; + } +} diff --git a/unpublishedScripts/marketplace/camera-move/_tooltips.js b/unpublishedScripts/marketplace/camera-move/_tooltips.js new file mode 100644 index 0000000000..c1df716d24 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_tooltips.js @@ -0,0 +1,147 @@ +/* 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) { + function TooltipManager(options) { + this.options = options; + assert(options.elements, 'TooltipManager constructor expects options.elements'); + assert(options.tooltips, 'TooltipManager constructor expects options.tooltips'); + var TOOLTIPS = this.TOOLTIPS = {}; + $(options.tooltips).find('[for]').each(function() { + var id = $(this).attr('for'); + TOOLTIPS[id] = $(this).html(); + }); + + var _self = this; + options.elements.each(function() { + var element = $($(this).closest('.tooltip-target').get(0) || this), + input = element.is('input, button') ? element : element.find('input, button'), + parent = element.is('.row, button') ? element : element.parent(), + id = element.attr('for') || input.prop('id'), + tip = TOOLTIPS[id] || element.prop('title'); + + var tooltipSide = element.data('tooltipSide'); + + debugPrint('binding tooltip', tooltipSide, element[0].nodeName, id || element[0], tip); + if (!tip) { + return log('missing tooltip:', this.nodeName, id || this.id || this.name || $(this).text()); + } + if (element.is('.tooltipstered')) { + log('already tooltipstered!?', this.id, this.name, id); + return; + } + var instance = element.tooltipster({ + theme: ['tooltipster-noir'], + side: tooltipSide || ( + input.is('button') ? 'top' : + input.closest('.slider').get(0) || input.closest('.column').get(0) ? ['top','bottom'] : + ['right','top','bottom', 'left'] + ), + content: tip, + updateAnimation: 'scale', + trigger: !options.testmode ? 'hover' : 'click', + distance: element.is('.slider.row') ? -20 : undefined,// element + delay: [500, 1000], + contentAsHTML: true, + interactive: options.testmode, + minWidth: options.viewport && options.viewport.min.width, + maxWidth: options.viewport && options.viewport.max.width, + }).tooltipster('instance'); + + instance.on('close', function(event) { + if (options.keepopen === element) { + debugPrint(event.type, 'canceling close keepopen === element', id); + event.stop(); + options.keepopen = null; + } + }); + instance.on('before', function(event) { + debugPrint(event.type, 'before', event); + !options.testmode && _self.closeAll(); + !options.enabled && event.stop(); + return; + }); + parent.find(':focusable, input, [tabindex], button, .control') + .add(parent) + .add(input.closest(':focusable, input, [tabindex]')) + .on({ + click: function(evt) { + if (input.is('button')) { + return setTimeout(instance.close.bind(instance,null),50); + } + options.keepopen = element; 0&&instance.open(); + }, + focus: instance.open.bind(instance, null), + blur: function(evt) { + instance.close(); _self.openFocusedTooltip(); + }, + }); + }); + Object.assign(this, { + 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, + }; + $.tooltipster.setDefaults(options); + log('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(); + }, + }); + } + return TooltipManager; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/app-camera-move.js b/unpublishedScripts/marketplace/camera-move/app-camera-move.js index 25c11e5a93..7ec38d6fab 100644 --- a/unpublishedScripts/marketplace/camera-move/app-camera-move.js +++ b/unpublishedScripts/marketplace/camera-move/app-camera-move.js @@ -9,7 +9,7 @@ // 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 */ +/* eslint-disable comma-dangle, no-empty */ "use strict"; var VERSION = '0.0.0d', @@ -19,7 +19,7 @@ var VERSION = '0.0.0d', text: '\nCam Drive', icon: Script.resolvePath('Eye-Camera.svg'), }, - DEFAULT_TOGGLE_KEY = '{ "text": "SPACE" }'; + DEFAULT_TOGGLE_KEY = { text: 'SPACE' }; var MINIMAL_CURSOR_SCALE = 0.5, FILENAME = Script.resolvePath(''), @@ -27,10 +27,7 @@ var MINIMAL_CURSOR_SCALE = 0.5, false || (FILENAME.match(/[&#?]debug[=](\w+)/)||[])[1] || Settings.getValue(NAMESPACE + '/debug') ), - EPSILON = 1e-6, - DEG_TO_RAD = Math.PI / 180.0; - -WANT_DEBUG = 1; + EPSILON = 1e-6; function log() { print( NAMESPACE + ' | ' + [].slice.call(arguments).join(' ') ); @@ -42,6 +39,7 @@ var require = Script.require, overlayDebugOutput = function(){}; if (WANT_DEBUG) { + log('WANT_DEBUG is true; instrumenting debug rigging', WANT_DEBUG); _instrumentDebugValues(); } @@ -49,7 +47,9 @@ 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'); + configUtils = require('./modules/config-utils.js'), + AvatarUpdater = require('./avatar-updater.js'); + Object.assign = Object.assign || _utils.assign; @@ -78,11 +78,13 @@ var DEFAULTS = { 'rotation-x-speed': 45, 'rotation-y-speed': 60, 'rotation-z-speed': 1, - 'rotation-mouse-multiplier': 1.0, - 'rotation-keyboard-multiplier': 1.0, + 'mouse-multiplier': 1.0, + 'keyboard-multiplier': 1.0, 'ui-enable-tooltips': true, 'ui-show-advanced-options': false, + + // 'toggle-key': DEFAULT_TOGGLE_KEY, }; // map setting names to/from corresponding Menu and API properties @@ -117,9 +119,19 @@ var APPLICATION_SETTINGS = { cameraControls.setEnabled(!!nv); }, }, + // 'toggle-key': { + // get: function() { try { + // return JSON.parse(cameraConfig.getValue('toggle-key')); + // } catch (e) { + // return DEFAULT_TOGGLE_KEY; + // } }, + // set: function(nv) { + // assert(typeof nv === 'object', 'new toggle-key is not an object: ' + nv); + // cameraConfig.setValue('toggle-key', JSON.parse(JSON.stringify(nv))); + // }, + // }, }; - var DEBUG_INFO = { // these values are also sent to the tablet app after EventBridge initialization appVersion: VERSION, @@ -157,9 +169,9 @@ var globalState = { // current input controls' effective velocities currentVelocities: new movementUtils.VelocityTracker({ translation: Vec3.ZERO, - step_translation: Vec3.ZERO, + step_translation: Vec3.ZERO, // eslint-disable-line camelcase rotation: Vec3.ZERO, - step_rotation: Vec3.ZERO, + step_rotation: Vec3.ZERO, // eslint-disable-line camelcase zoom: Vec3.ZERO, }), }; @@ -186,9 +198,11 @@ function main() { defaultValues: DEFAULTS, }); - var toggleKey = JSON.parse(DEFAULT_TOGGLE_KEY); + var toggleKey = DEFAULT_TOGGLE_KEY; if (cameraConfig.getValue('toggle-key')) { - try { toggleKey = JSON.parse(cameraConfig.getValue('toggle-key')); } catch(e) {} + try { + toggleKey = JSON.parse(cameraConfig.getValue('toggle-key')); + } catch (e) {} } // set up a monitor to observe configuration changes between the two sources var MONITOR_INTERVAL_MS = 1000; @@ -219,11 +233,11 @@ function main() { }); settingsApp.onUnhandledMessage = function(msg) { - switch(msg.method) { + switch (msg.method) { case 'window.close': { this.toggle(false); } break; - case 'reloadClientScript': { + case 'reloadClientScript': { log('reloadClientScript...'); _utils.reloadClientScript(FILENAME); } break; @@ -236,10 +250,9 @@ function main() { }, 500); } break; case 'reset': { - //if (!Window.confirm('Reset all camera move settings to system defaults?')) { + // if (!Window.confirm('Reset all camera move settings to system defaults?')) { // return; - //} - var novalue = Uuid.generate(); + // } var resetValues = {}; Object.keys(DEFAULTS).reduce(function(out, key) { var resolved = cameraConfig.resolve(key); @@ -259,7 +272,7 @@ function main() { } } break; case 'overlayWebWindow': { - _overlayWebWindow(msg.options); + _utils._overlayWebWindow(msg.options); } break; default: { log('onUnhandledMessage', JSON.stringify(msg,0,2)); @@ -273,8 +286,8 @@ function main() { namespace: DEFAULTS.namespace, mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'), xexcludeNames: [ 'Keyboard.C', 'Keyboard.E', 'Actions.TranslateY' ], - mouseMultiplier: cameraConfig.getValue('rotation-mouse-multiplier'), - keyboardMultiplier: cameraConfig.getValue('rotation-keyboard-multiplier'), + mouseMultiplier: cameraConfig.getValue('mouse-multiplier'), + keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'), eventFilter: function eventFilter(from, event, defaultFilter) { var result = defaultFilter(from, event), driveKeyName = event.driveKeyName; @@ -299,15 +312,21 @@ function main() { Script.scriptEnding.connect(eventMapper, 'disable'); applicationConfig.register({ 'enable-mouse-smooth': { object: [ eventMapper.options, 'mouseSmooth' ] }, - 'rotation-keyboard-multiplier': { object: [ eventMapper.options, 'keyboardMultiplier' ] }, - 'rotation-mouse-multiplier': { object: [ eventMapper.options, 'mouseMultiplier' ] }, + '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: update, + update: avatarUpdater, threadMode: cameraConfig.getValue('thread-update-mode'), fps: cameraConfig.getValue('fps'), getRuntimeSeconds: _utils.getRuntimeSeconds, @@ -358,7 +377,7 @@ function main() { function updateButtonText() { var lines = [ settingsApp.isActive ? '(app open)' : '', - cameraControls.enabled ? (update.momentaryFPS||0).toFixed(2) + 'fps' : BUTTON_CONFIG.text.trim() + cameraControls.enabled ? (avatarUpdater.update.momentaryFPS||0).toFixed(2) + 'fps' : BUTTON_CONFIG.text.trim() ]; button && button.editProperties({ text: lines.join('\n') }); } @@ -375,15 +394,15 @@ function main() { fpsTimeout = 0; } eventMapper.disable(); - _resetMyAvatarMotor({ MyAvatar: MyAvatar }); + avatarUpdater._resetMyAvatarMotor({ MyAvatar: MyAvatar }); updateButtonText(); } - cameraConfig.getValue('debug') && Overlays.editOverlay(overlayDebugOutput.overlayID, { visible: enabled }); + overlayDebugOutput.overlayID && Overlays.editOverlay(overlayDebugOutput.overlayID, { visible: enabled }); }); var resetIfChanged = [ 'minimal-cursor', 'drive-mode', 'fps', 'thread-update-mode', - 'rotation-mouse-multiplier', 'rotation-keyboard-multiplier', + 'mouse-multiplier', 'keyboard-multiplier', 'enable-mouse-smooth', 'constant-delta-time', ].filter(Boolean).map(_utils.bind(cameraConfig, 'resolve')); @@ -430,7 +449,7 @@ function onCameraControlsEnabled() { Reticle.scale = MINIMAL_CURSOR_SCALE; } log('cameraConfig', JSON.stringify({ - cameraConfig: getCameraMovementSettings(cameraConfig), + cameraConfig: getCameraMovementSettings(), //DEFAULTS: DEFAULTS })); } @@ -463,20 +482,22 @@ function onCameraModeChanged(mode, oldMode) { } // consolidate and normalize cameraConfig settings -function getCameraMovementSettings(cameraConfig) { +function getCameraMovementSettings() { return { epsilon: EPSILON, debug: cameraConfig.getValue('debug'), 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('rotation-mouse-multiplier'), - keyboardMultiplier: cameraConfig.getValue('rotation-keyboard-multiplier'), + mouseMultiplier: cameraConfig.getValue('mouse-multiplier'), + keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'), rotation: _getEasingGroup(cameraConfig, 'rotation'), translation: _getEasingGroup(cameraConfig, 'translation'), @@ -489,12 +510,8 @@ function getCameraMovementSettings(cameraConfig) { if (group === 'zoom') { // BoomIn / TranslateCameraZ support is only partially plumbed -- for now use scaled translation easings group = 'translation'; - multiplier = 0.01; - } else if (group === 'rotation') { - // degrees -> radians - //multiplier = DEG_TO_RAD; + multiplier = 0.001; } - return { easeIn: cameraConfig.getFloat(group + '-ease-in'), easeOut: cameraConfig.getFloat(group + '-ease-out'), @@ -508,232 +525,7 @@ function getCameraMovementSettings(cameraConfig) { } } -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 - -update.frameCount = 0; -update.endTime = _utils.getRuntimeSeconds(); - -function update(dt) { - update.frameCount++; - var startTime = _utils.getRuntimeSeconds(); - var settings = getCameraMovementSettings(cameraConfig); - - var collisions = applicationConfig.getValue('Avatar/Enable Avatar Collisions'), - independentCamera = Camera.mode === 'independent', - headPitch = MyAvatar.headPitch; - - var actualDeltaTime = Math.max(MIN_DELTA_TIME, (startTime - update.endTime)), - deltaTime; - - if (settings.useConstantDeltaTime) { - deltaTime = settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? - (1 / cameraControls.fps) : (1 / 90); - } else if (settings.threadMode === movementUtils.CameraControls.SCRIPT_UPDATE) { - deltaTime = dt; - } else { - deltaTime = actualDeltaTime; - } - - 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 = eventMapper.getState({ 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.JITTER_TEST: - case DriveModes.POSITION: { - pendingChanges.MyAvatar.position = Vec3.sum(currentPosition, deltaPosition); - break; - } - default: { - throw new Error('unknown driveMode: ' + settings.driveMode); - break; - } - } - } - - 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.driveMode === movementUtils.DriveModes.JITTER_TEST) { - 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; - - var submitted = pendingChanges.submit(); - - update.momentaryFPS = 1 / actualDeltaTime; - - if (settings.debug && update.frameCount % 120 === 0) { - Messages.sendLocalMessage(_debugChannel, JSON.stringify({ - threadMode: cameraControls.threadMode, - driveMode: settings.driveMode, - orientationProperty: orientationProperty, - isGrounded: movementState.isGrounded, - targetAnimationFPS: cameraControls.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? cameraControls.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)); - } -} - -if (0) { - Script.update.connect(gc); - Script.scriptEnding.connect(function() { - Script.update.disconnect(gc); - }); -} - -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; -} +// ---------------------------------------------------------------------------- // ---------------------------------------------------------------------------- function _startConfigationMonitor(applicationConfig, cameraConfig, interval) { @@ -753,20 +545,6 @@ function _startConfigationMonitor(applicationConfig, cameraConfig, interval) { }, interval); } -// ---------------------------------------------------------------------------- - -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 _fixedPrecisionStringifiyFilter(key, value, object) { if (typeof value === 'object' && value && 'w' in value) { @@ -789,7 +567,7 @@ function _createOverlayDebugOutput(options) { Script.scriptEnding.connect(function() { Overlays.deleteOverlay(overlayDebugOutput.overlayID); - Messages.unsubscribe(debugChannel); + Messages.unsubscribe(_debugChannel); Messages.messageReceived.disconnect(onMessageReceived); }); function overlayDebugOutput(output) { @@ -835,8 +613,8 @@ function _instrumentDebugValues() { require = Script.require(Script.resolvePath('./modules/_utils.js') + cacheBuster).makeDebugRequire(Script.resolvePath('.')); APP_HTML_URL += cacheBuster; overlayDebugOutput = _createOverlayDebugOutput({ - lineHeight: 10, - font: { size: 10 }, + lineHeight: 12, + font: { size: 12 }, width: 250, height: 800 }); // auto-disable camera move mode when debugging Script.scriptEnding.connect(function() { @@ -847,11 +625,13 @@ function _instrumentDebugValues() { // Show fatal (unhandled) exceptions in a BSOD popup Script.unhandledException.connect(function onUnhandledException(error) { log('UNHANDLED EXCEPTION!!', error, error && error.stack); - try { cameraControls.disable(); } catch(e) {} + try { + cameraControls.disable(); + } catch (e) {} Script.unhandledException.disconnect(onUnhandledException); if (WANT_DEBUG) { // show blue screen of death with the error details - _utils.BSOD({ + new _utils.BSOD({ error: error, buttons: [ 'Abort', 'Retry', 'Fail' ], debugInfo: DEBUG_INFO, diff --git a/unpublishedScripts/marketplace/camera-move/app.html b/unpublishedScripts/marketplace/camera-move/app.html index c27e179df5..a86813421d 100644 --- a/unpublishedScripts/marketplace/camera-move/app.html +++ b/unpublishedScripts/marketplace/camera-move/app.html @@ -10,13 +10,14 @@ .content { display: none } - + + @@ -33,19 +34,24 @@ browserUtils = new _utils.BrowserUtils(window); } + + + + + +} catch(e) { alert(e) } -
@@ -144,12 +173,6 @@
- -
-          
-        
- -

Translation

@@ -158,12 +181,12 @@
-
+
-
+
@@ -178,12 +201,12 @@
-
+
-
+
@@ -193,11 +216,11 @@

Options

- +
- +
@@ -220,11 +243,11 @@
- +
- +
@@ -235,12 +258,12 @@

input scaling

- - + +
- - + +
@@ -270,11 +293,11 @@
- +
- +
@@ -289,21 +312,21 @@

Update Mode

-
+
- +
- +
- +
- +
@@ -311,22 +334,22 @@

Script update mode: -
+
- +
- -
  fps: + +
  fps:
- +
- +
@@ -337,11 +360,11 @@

debug menu

-
+
- - + +
@@ -362,9 +385,37 @@
keybinding:  
+ + + + + + + + + + +
+ +

+            
+
- +

@@ -453,8 +504,8 @@
degrees per up/down controller inputs
degrees per left/right controller inputs
-
prescale raw controller inputs by this amount
-
prescale raw controller inputs by this amount
+
prescale raw controller inputs by this amount
+
prescale raw controller inputs by this amount
@@ -495,14 +546,44 @@ replace current settings with previously-exported JSON
- trigger MyAvatar > Reset Sensors and then reset bodyPitch and bodyYaw + trigger MyAvatar > Reset Sensors;reset bodyPitch and bodyYaw
-
+
show / hide advanced settings
+ + + + - + -