src/App.js
import * as ta from './three/Additions.js'
import dom from './DOM.js'
import som from './SOM.js'
import Router from './Router.js'
import Component from './Component.js'
import Localizer from './Localizer.js'
import Engine from './display/Engine.js'
import AssetLoader from './AssetLoader.js'
import EventHandler from './EventHandler.js'
import FlatDisplay from './display/FlatDisplay.js'
import { throttledConsoleLog } from './throttle.js'
import DisplayModeTracker from './DisplayModeTracker.js'
import * as displayConstants from './display/Constants.js'
import Stylist from './style/Stylist.js'
import ActionMap from 'action-input/src/action/ActionMap'
import ClickFilter from 'action-input/src/filter/ClickFilter'
import MinMaxFilter from 'action-input/src/filter/MinMaxFilter'
import ActionManager from 'action-input/src/action/ActionManager'
import MouseInputSource from 'action-input/src/input/MouseInputSource'
import TouchInputSource from 'action-input/src/input/TouchInputSource'
import GamepadInputSource from 'action-input/src/input/GamepadInputSource'
import KeyboardInputSource from 'action-input/src/input/KeyboardInputSource'
import TextInputSource from './input/TextInputSource.js'
import ActivePickFilter from './input/ActivePickFilter.js'
import PickingInputSource from './input/PickingInputSource.js'
/**
* App contains the orchestration logic for the entirety of what is being displayed for a given app, including the app chrome like navigation.
*
* It contains the root data structures for each display mode:
*
* - For flat mode it holds a DOM element.
* - For portal mode it holds a DOM element for overlay controls and a 3D scene for spatial controls and virtual environments.
* - For immersive mode it holds a 3D scene for spatial controls as well as virtual environments.
*
* It manages WebXR sessions for portal and immersive modes.
* It also toggles the visibility of the flat and portal DOM fragments as display modes change.
*
* App communicates these changes to {@link Component}s via events so that they may react.
*/
const App = class extends EventHandler {
/**
@param {Object} [options]
@param {Component} [options.textInputComponent=null]
*/
constructor(options) {
super()
this._options = Object.assign(
{
textInputComponent: null
},
options
)
this._handlePortalTick = this._handlePortalTick.bind(this)
this._handleImmersiveTick = this._handleImmersiveTick.bind(this)
this._handleFlatDisplayTick = this._handleFlatDisplayTick.bind(this)
this._handleWindowMessage = this._handleWindowMessage.bind(this)
this._stylist = new Stylist()
this._stylist.addListener((eventName, stylist) => {
setInterval(() => {
switch (this.displayMode) {
case App.IMMERSIVE:
this._stylist.style(this._immersiveScene, this._immersiveEngine.renderer)
break
case App.PORTAL:
this._stylist.style(this._portalScene, this._portalEngine.renderer)
break
case App.FLAT:
if (this._debugScene !== null) {
this._stylist.style(this._debugScene, this._flatDisplay.renderer)
}
break
}
}, 100)
}, Stylist.LINKS_LOADED_EVENT)
this._stylist.loadLinks()
this._router = new Router()
this._assetLoader = AssetLoader.Singleton
this._displayModeTracker = DisplayModeTracker.Singleton
this._displayMode = App.FLAT
this._pickingInputSource = new PickingInputSource()
this._actionManager = new ActionManager(false)
this._actionManager.addFilter('click', new ClickFilter(this._actionManager.queryInputPath))
this._actionManager.addFilter('active-pick', new ActivePickFilter(this._actionManager.queryInputPath))
this._actionManager.addFilter('min-max', new MinMaxFilter())
this._actionManager.addInputSource('picking', this._pickingInputSource)
this._actionManager.addInputSource('mouse', new MouseInputSource())
this._actionManager.addInputSource('touch', new TouchInputSource())
this._actionManager.addInputSource('gamepad', new GamepadInputSource())
this._actionManager.addInputSource('keyboard', new KeyboardInputSource())
this._actionManager.addInputSource('text', Component.TextInputReceiver.textInputSource)
/** @todo figure out how action map files should be bundled */
this._actionManager.addActionMap(
'flat',
new ActionMap([...this._actionManager.filters], '/static/potassium-es/actions/flat-action-map.json')
)
this._actionManager.addActionMap(
'portal',
new ActionMap([...this._actionManager.filters], '/static/potassium-es/actions/portal-action-map.json')
)
this._actionManager.addActionMap(
'immersive',
new ActionMap([...this._actionManager.filters], '/static/potassium-es/actions/immersive-action-map.json')
)
/** the 'flat-dev' action map is used during dev when App.toggleFlatDisplay is used */
this._actionManager.addActionMap(
'flat-dev',
new ActionMap([...this._actionManager.filters], '/static/potassium-es/actions/flat-dev-action-map.json')
)
this._actionManager.switchToActionMaps('flat')
// Route activate actions to the target Component
this._actionManager.addActionListener(
'/action/activate',
(actionPath, active, value, actionParameters, filterParameters, inputSource) => {
if (value) {
value.handleAction(actionPath, active, value, actionParameters, filterParameters, inputSource)
}
}
)
this._actionManager.addActionListener(
'/action/activate-dom',
(actionPath, active, value, actionParameters, filterParameters, inputSource) => {
if (value) {
value.handleAction('/action/activate', active, value, actionParameters, filterParameters, inputSource)
}
}
)
// Route text input actions to the Component that has text input focus
this._actionManager.addActionListener(
'/action/text-input',
(actionPath, active, value, actionParameters, filterParameters, inputSource) => {
if (active && Component.TextInputFocus !== null) {
Component.TextInputFocus.handleAction(
actionPath,
active,
value,
actionParameters,
filterParameters,
inputSource
)
}
}
)
// Route flat-dev actions for moving around the camera in FlatDisplay
this._actionManager.addActionListener(
'/action/transform-scene',
(actionPath, active, value, actionParameters, filterParameters, inputSource) => {
if (this._flatCamera === null) return
if (active === false) {
this._flatTransformation = null
this._flatClock.stop()
return
}
this._flatClock.start() // resets delta to zero
this._flatTransformation = {
reset: actionParameters.reset === true,
translation: actionParameters.translation || null,
rotation: actionParameters.rotation || null
}
if (actionParameters.translation) {
this._flatTransformation.translation = _calculateTranslation(
actionParameters.translation,
this._flatCamera.quaternion
)
}
if (actionParameters.rotation) {
this._flatTransformation.rotation = new THREE.Quaternion().setFromEuler(
new THREE.Euler(...actionParameters.rotation)
)
}
}
)
// The engines call back from their raf loops, but in flat mode the App uses window.requestAnimationFrame to call ActionManager.poll
this._handleWindowAnimationFrame = this._handleWindowAnimationFrame.bind(this)
/**
The root DOM elmenent that will contain everything for every display mode
Add this to your page's DOM
*/
this._dom = dom.div({ class: 'app page-app' })
/** Flat display mode DOM elements */
this._flatDOM = dom
.div({
class: 'flat-root dom-root'
})
.appendTo(this._dom)
this._flatDOM.setAttribute('data-name', 'FlatRoot')
/** Portal display mode overlay DOM */
this._portalDOM = dom
.div({
class: 'portal-root dom-root'
})
.appendTo(this._dom)
this._portalDOM.setAttribute('data-name', 'PortalRoot')
/** Portal display mode 3D scene */
this._portalScene = som.scene()
this._portalScene.addClass('portal-scene', 'app', 'spatial-app')
this._portalScene.name = 'PortalScene'
this._portalEngine = new Engine(this._portalScene, displayConstants.PORTAL, this._handlePortalTick)
this._portalEngine.addListener((eventName, engine) => {
if (this._displayMode === App.PORTAL) {
this.setDisplayMode(App.FLAT)
}
}, Engine.STOPPED)
/** Portal display Spatial Object Model (SOM) container */
this._portalSOM = som.group().appendTo(this._portalScene)
this._portalSOM.addClass('som-root', 'portal-root', 'portal-som')
this._portalSOM.name = 'PortalSOM'
/** Immersive display mode 3D scene */
this._immersiveScene = som.scene()
this._immersiveScene.addClass('immersive-scene', 'app', 'spatial-app')
this._immersiveScene.name = 'ImmersiveScene'
this._immersiveEngine = new Engine(this._immersiveScene, displayConstants.IMMERSIVE, this._handleImmersiveTick)
this._immersiveEngine.addListener((eventName, engine) => {
if (this._displayMode === App.IMMERSIVE) {
this.setDisplayMode(App.FLAT)
}
}, Engine.STOPPED)
/** Immersive display Spatial Object Model (SOM) container */
this._immersiveSOM = som.group().appendTo(this._immersiveScene)
this._immersiveSOM.addClass('som-root', 'immersive-root', 'immersive-som')
this._immersiveSOM.name = 'ImmersiveSOM'
/* Set up WebXR, WebVR, or fallback based displays for the portal and immersive engines */
Engine.chooseDisplays(this._portalEngine, this._immersiveEngine)
.then(() => {
this._displayModeTracker.setModes(true, this._portalEngine.hasDisplay, this._immersiveEngine.hasDisplay)
})
.catch(err => {
console.error('Error setting engine displays', err)
})
/*
_flatDisplay is populated if you you call App.toggleFlatDisplay(...)
@type {Engine.SceneDisplay}
*/
this._flatDisplay = null
this._debugScene = null // either _portalScene or _immersiveScene
this._flatCamera = null // a THREE.Camera
this._flatClock = null // a THREE.Clock
/* _flatTransformation is used to transform the camera during dev based on input triggered actions */
this._flatTransformation = null
/* Set up hands and pointers */
this._leftHand = som.group(this._makeHand(0x9999ff)).appendTo(this._immersiveScene)
this._leftHand.addClass('left-hand')
this._leftHand.name = 'LeftHand'
this._leftHand.visible = false
this._leftPointer = this._makePointer(0x99ff99)
this._leftPointer.addClass('left-pointer')
this._leftPointer.name = 'LeftPointer'
this._leftPointer.visible = false
this._leftHand.add(this._leftPointer)
this._rightHand = som.group(this._makeHand(0xff9999)).appendTo(this._immersiveScene)
this._rightHand.addClass('right-hand')
this._rightHand.name = 'RightHand'
this._rightHand.visible = false
this._rightPointer = this._makePointer(0x99ff99)
this._rightPointer.addClass('right-pointer')
this._rightPointer.name = 'RightPointer'
this._rightPointer.visible = false
this._rightHand.add(this._rightPointer)
/* Set up text input group */
this._immersiveInputGroup = som.group().addClass('som-input-group')
this._immersiveInputGroup.name = 'InputGroup'
this._immersiveScene.add(this._immersiveInputGroup)
if (this._options.textInputComponent !== null) {
this._immersiveInputGroup.add(this._options.textInputComponent.immersiveSOM)
}
// When the mode changes, notify all of the children Components
this.addListener((eventName, mode) => {
this._actionManager.switchToActionMaps(mode)
/* Component listens for events on the DisplayModeTracker and updates itself accordingly */
DisplayModeTracker.Singleton.displayMode = mode
/* style once right at the start to avoid seeing unstyled scenes */
switch (mode) {
case App.IMMERSIVE:
this._stylist.style(this._immersiveScene, this._immersiveEngine.renderer)
break
case App.PORTAL:
this._stylist.style(this._portalScene, this._portalEngine.renderer)
break
}
}, App.DisplayModeChangedEvent)
this._updateClasses()
window.requestAnimationFrame(this._handleWindowAnimationFrame)
// Listen for messages from the potassium-inspector WebExtension
window.addEventListener('message', this._handleWindowMessage)
}
/** @type {Router} */
get router() {
return this._router
}
/** @type {AssetLoader} */
get assetLoader() {
return this._assetLoader
}
/** @type {HTMLElement} */
get dom() {
return this._dom
}
/** @type {HTMLElement} */
get flatDOM() {
return this._flatDOM
}
/** @type {HTMLElement} */
get portalDOM() {
return this._portalDOM
}
/** @type {THREE.Group} */
get portalScene() {
return this._portalScene
}
/** @type {THREE.Group} */
get portalSOM() {
return this._portalSOM
}
/** @type {THREE.Group} */
get immersiveScene() {
return this._immersiveScene
}
/** @type {THREE.Group} */
get immersiveSOM() {
return this._immersiveSOM
}
/** @type {ActionManager} */
get actionManager() {
return this._actionManager
}
/**
appendComponent adds the childComponent's flatDOM, portalDOM, portalSOM, and immersiveSOM to this Component's equivalent attributes.
@param {Component} childComponent
*/
appendComponent(childComponent) {
this._flatDOM.appendChild(childComponent.flatDOM)
this._portalDOM.appendChild(childComponent.portalDOM)
this._portalSOM.add(childComponent.portalSOM)
this._immersiveSOM.add(childComponent.immersiveSOM)
}
/*
removeComponent removes the childComponent's flatDOM, portalDOM, portalSOM, and immersiveSOM from this Component's equivalent attributes.
@param {Component} childComponent
*/
removeComponent(childComponent) {
this._flatDOM.removeChild(childComponent.flatDOM)
this._portalDOM.removeChild(childComponent.portalDOM)
this._portalSOM.remove(childComponent.portalSOM)
this._immersiveSOM.remove(childComponent.immersiveSOM)
}
/** @type {string} flat|portal|immersive */
get displayMode() {
return this._displayMode
}
/**
@param {string} value flat|portal|immersive
@return {Promise<string>} display mode
*/
setDisplayMode(value) {
if (this._displayMode === value)
return new Promise((resolve, reject) => {
resolve(this._displayMode)
})
if (value === App.FLAT) {
return new Promise((resolve, reject) => {
this._displayMode = App.FLAT
this._portalEngine.stop()
this._immersiveEngine.stop()
this._updateClasses()
this.trigger(App.DisplayModeChangedEvent, App.FLAT)
this._displayModeTracker.currentDisplayMode = App.FLAT
window.requestAnimationFrame(this._handleWindowAnimationFrame)
resolve(App.FLAT)
})
}
if (value === App.PORTAL) {
return new Promise((resolve, reject) => {
this._immersiveEngine.stop()
this._portalEngine
.start()
.then(() => {
this._displayMode = App.PORTAL
this._updateClasses()
this.trigger(App.DisplayModeChangedEvent, App.PORTAL)
this._displayModeTracker.currentDisplayMode = App.PORTAL
resolve(App.PORTAL)
})
.catch(err => {
this.trigger(App.DisplayModeFailedEvent, App.PORTAL)
reject(err)
})
})
}
if (value === App.IMMERSIVE) {
return new Promise((resolve, reject) => {
this._portalEngine.stop()
this._immersiveEngine
.start()
.then(() => {
this._displayMode = App.IMMERSIVE
this._updateClasses()
this.trigger(App.DisplayModeChangedEvent, App.IMMERSIVE)
this._displayModeTracker.currentDisplayMode = App.IMMERSIVE
resolve(App.IMMERSIVE)
})
.catch(err => {
this.trigger(App.DisplayModeFailedEvent, App.IMMERSIVE)
reject(err)
})
})
}
throw new Error('Unhandled display mode', value)
}
toggleEdges() {
if (this._debugScene !== null) {
this._debugScene.toggleEdges(true)
return
}
switch (this.displayMode) {
case App.FLAT:
return
case App.PORTAL:
this.portalSOM.toggleEdges(true)
return
case App.IMMERSIVE:
this.immersiveSOM.toggleEdges(true)
return
}
}
/**
toggleFlatDisplay enables creators to see a debugging view into the immersive scene on their flat screens.
This is handy for coding and styling spatial controls when a headset is not available or you are having a good hair day and don't want to mess with success.
@param {bool} [show=null] - if true, create and show the display, otherwise tear it down
@param {bool} [immersive=true] - if true then show the immersive scene, otherwise show the portal scene
*/
toggleFlatDisplay(show = null, immersive = true) {
if (show === null) {
show = this._flatDisplay === null ? true : false
}
if (show) {
if (this._flatDisplay !== null) {
if (immersive) {
if (this._debugScene === this._immersiveScene) return
} else {
if (this._debugScene === this._portalScene) return
}
document.body.removeChild(this._flatDisplay.dom)
this._flatDisplay.stop()
}
this._dom
.removeClass('flat-mode', 'immersive-mode', 'portal-mode')
.addClass(immersive ? 'immersive-mode' : 'portal-mode')
this._debugScene = immersive ? this._immersiveScene : this._portalScene
this._flatCamera = som.perspectiveCamera([45, 1, 0.05, 10000])
this._flatClock = new THREE.Clock(false)
this._flatCamera.name = 'flat-camera'
this._flatCamera.matrixAutoUpdate = true
this._flatDisplay = new FlatDisplay(this._flatCamera, this._debugScene, this._handleFlatDisplayTick)
this._stylist.style(this._debugScene, this._flatDisplay.renderer)
document.body.prepend(this._flatDisplay.dom)
this._flatDisplay.start()
this._actionManager.activateActionMaps('flat-dev')
this._displayModeTracker.currentDisplayMode = immersive ? App.IMMERSIVE : App.PORTAL
} else {
if (this._flatDisplay === null) return
this._dom.removeClass('immersive-mode', 'portal-mode').addClass('flat-mode')
document.body.removeChild(this._flatDisplay.dom)
this._flatDisplay.stop()
this._flatDisplay = null
this._flatCamera = null
this._flatClock = null
this._debugScene = null
this._actionManager.deactivateActionMaps('flat-dev')
this._displayModeTracker.currentDisplayMode = App.FLAT
}
}
set localizerGathering(shouldGather) {
Localizer.Singleton.gathering = shouldGather
}
get localizerGathering() {
return Localizer.Singleton.gathering
}
get localizerGatheredData() {
return Localizer.Singleton.gatheredData
}
/**
The potassium-inspector sends messages using window.postMessage this method watches for them
*/
_handleWindowMessage(event) {
switch (event.data.action) {
case App.GetKSSAction:
const rawKSS = this._stylist.stylesheets[0] ? this._stylist.stylesheets[0].raw : ''
window.postMessage(
{
action: App.PutKSSAction,
kss: rawKSS
},
'*'
)
break
case App.GetStyleTreeAction:
const styleTree =
event.data.scene === 'portal' ? this._portalScene.getStyleTree() : this._immersiveScene.getStyleTree()
window.postMessage(
{
tree: styleTree,
action: App.PutStyleTreeAction
},
'*'
)
break
case App.ShowFlatDisplayAction:
this.toggleFlatDisplay(true, event.data.display !== 'portal')
break
case App.HideFlatDisplayAction:
this.toggleFlatDisplay(false)
break
case App.ToggleEdgesAction:
this.toggleEdges()
break
}
}
/** Called while showing the debug flat display */
_handleFlatDisplayTick() {
if (this._flatCamera === null || this._flatTransformation === null) return
if (this._flatTransformation.reset) {
if (this._flatTransformation.translation) {
this._debugScene.position.set(...this._flatTransformation.translation)
} else {
this._debugScene.position.set(0, 0, 0)
}
if (this._flatTransformation.rotation) {
this._debugScene.quaternion.setFromEuler(new THREE.Euler(...this._flatTransformation.rotation))
} else {
this._debugScene.quaternion.set(0, 0, 0, 1)
}
return
} else {
const delta = this._flatClock.getDelta()
if (this._flatTransformation.rotation) {
this._debugScene.quaternion.multiply(this._flatTransformation.rotation)
}
if (this._flatTransformation.translation) {
this._debugScene.position.set(
this._debugScene.position.x + this._flatTransformation.translation[0] * delta,
this._debugScene.position.y + this._flatTransformation.translation[1] * delta,
this._debugScene.position.z + this._flatTransformation.translation[2] * delta
)
}
}
}
_makeHand(color) {
/** @todo make this a portable resource, perhaps by embedding it in an ES module */
return som.obj(
'/static/potassium-es/models/Controller.obj',
(group, obj) => {
const body = group.getObjectByName('Body_Cylinder') // Magic string for temp OBJ
if (!body) {
console.error('Did not find a hand group to color', group)
return
}
body.material.color.set(color)
},
(...params) => {
console.error('Error loading hands', ...params)
}
)
}
_makePointer(color) {
const material = som.lineBasicMaterial({ color: color })
const geometry = som.geometry()
geometry.vertices.push(som.vector3(0, 0, 0), som.vector3(0, 0, -1000))
const pointer = som.line(geometry, material)
pointer.name = 'pointer'
return pointer
}
_handlePortalTick() {
// Update picking
this._pickingInputSource.clearIntersectObjects()
this._actionManager.queryInputPath('/input/touch/normalized-position/0', _workingQueryArray_1)
if (_workingQueryArray_1[0]) {
this._pickingInputSource.touch = this._portalEngine.pickScreen(_workingQueryArray_1[1][0], _workingQueryArray_1[1][1])
}
// Update actions
this._actionManager.poll()
}
_handleImmersiveTick() {
// Update hand poses, visibility, and pointers
this._actionManager.queryInputPath('/input/gamepad/left/position', _workingQueryArray_1)
if (_workingQueryArray_1[0]) {
this._leftHand.position.set(_workingQueryArray_1[1][0], _workingQueryArray_1[1][1], _workingQueryArray_1[1][2])
} else {
this._leftHand.position.set(
App.DefaultLeftHandPosition[0],
App.DefaultLeftHandPosition[1],
App.DefaultLeftHandPosition[2]
)
}
this._actionManager.queryInputPath('/input/gamepad/left/orientation', _workingQueryArray_1)
if (_workingQueryArray_1[0]) {
this._leftHand.quaternion.set(
_workingQueryArray_1[1][0],
_workingQueryArray_1[1][1],
_workingQueryArray_1[1][2],
_workingQueryArray_1[1][3]
)
this._actionManager.queryInputPath('/input/gamepad/left/button/0/touched', _workingQueryArray_1)
this._actionManager.queryInputPath('/input/gamepad/left/button/4/touched', _workingQueryArray_2)
this._leftPointer.visible = _workingQueryArray_1[0] || _workingQueryArray_2[0]
this._leftHand.visible = true
} else {
// If it's not at least a 3dof controller, we don't show it
this._leftHand.visible = false
}
this._actionManager.queryInputPath('/input/gamepad/right/position', _workingQueryArray_1)
if (_workingQueryArray_1[0]) {
this._rightHand.position.set(_workingQueryArray_1[1][0], _workingQueryArray_1[1][1], _workingQueryArray_1[1][2])
} else {
this._rightHand.position.set(
App.DefaultRightHandPosition[0],
App.DefaultRightHandPosition[1],
App.DefaultRightHandPosition[2]
)
}
this._actionManager.queryInputPath('/input/gamepad/right/orientation', _workingQueryArray_1)
if (_workingQueryArray_1[0]) {
this._rightHand.quaternion.set(
_workingQueryArray_1[1][0],
_workingQueryArray_1[1][1],
_workingQueryArray_1[1][2],
_workingQueryArray_1[1][3]
)
this._actionManager.queryInputPath('/input/gamepad/right/button/0/touched', _workingQueryArray_1)
this._actionManager.queryInputPath('/input/gamepad/right/button/4/touched', _workingQueryArray_2)
this._rightPointer.visible = _workingQueryArray_1[0] || _workingQueryArray_2[0]
this._rightHand.visible = true
} else {
// If it's not at least a 3dof controller, we don't show it
this._rightHand.visible = false
}
// Update picking
this._pickingInputSource.clearIntersectObjects()
if (this._leftHand.visible && this._leftPointer.visible) {
// Turn off the hand during picking
this._leftHand.visible = false
this._pickingInputSource.left = this._immersiveEngine.pickPose(this._leftPointer)
this._leftHand.visible = true
} else {
this._pickingInputSource.left = null
}
if (this._rightHand.visible && this._rightPointer.visible) {
// Turn off the hand during picking
this._rightHand.visible = false
this._pickingInputSource.right = this._immersiveEngine.pickPose(this._rightPointer)
this._rightHand.visible = true
} else {
this._pickingInputSource.right = null
}
this._actionManager.poll()
}
_handleWindowAnimationFrame() {
if (this._displayMode !== App.FLAT) return
window.requestAnimationFrame(this._handleWindowAnimationFrame)
this._actionManager.poll()
}
_updateClasses() {
this._dom.removeClass('flat-mode')
this._dom.removeClass('portal-mode')
this._dom.removeClass('immersive-mode')
this._dom.addClass(this._displayMode + '-mode')
}
}
/** Actions for messages between the page and the potassium-inspector WebExtension */
App.GetKSSAction = 'getKSS'
App.PutKSSAction = 'putKSS'
App.GetStyleTreeAction = 'getStyleTree'
App.PutStyleTreeAction = 'putStyleTree'
App.ShowFlatDisplayAction = 'showFlatDisplay'
App.HideFlatDisplayAction = 'hideFlatDisplay'
App.ToggleEdgesAction = 'toggleEdges'
App.DefaultLeftHandPosition = [-0.1, -0.4, -0.2]
App.DefaultRightHandPosition = [0.1, -0.4, -0.2]
App.FLAT = 'flat'
App.PORTAL = 'portal'
App.IMMERSIVE = 'immersive'
App.DISPLAY_MODES = [App.FLAT, App.PORTAL, App.IMMERSIVE]
App.DisplayModeChangedEvent = 'display-mode-changed'
App.DisplayModeFailedEvent = 'display-mode-failed'
export default App
const _yAxis = new THREE.Vector3(0, 1, 0)
const _zeroVector3 = new THREE.Vector3(0, 0, 0)
const _workingVector3_1 = new THREE.Vector3()
const _workingVector3_2 = new THREE.Vector3()
const _workingMatrix4_1 = new THREE.Matrix4()
const _workingQueryArray_1 = new Array(2)
const _workingQueryArray_2 = new Array(2)
/**
@param {number[]} inputTranslation [x, y, z]
@param {THREE.Quaternion} orientation
@return {number[]?} the orientated output translation
*/
const _calculateTranslation = function(inputTranslation, orientation) {
// set up the input vector
_workingVector3_1.set(...inputTranslation)
_workingVector3_1.x *= -1
if (_workingVector3_1.length() <= 0) return null
// Get the orientation vector
_workingVector3_2.set(0, 0, 1)
_workingVector3_2.applyQuaternion(orientation)
// Get the rotation matrix from origin to the orientation
_workingMatrix4_1.lookAt(_zeroVector3, _workingVector3_2, _yAxis)
// Apply the rotation matrix to the input vector
_workingVector3_1.applyMatrix4(_workingMatrix4_1)
_workingVector3_1.x *= -1
_workingVector3_1.y *= -1
return _workingVector3_1.toArray()
}