text: 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()
  }
  