added more documentation
/ test (push) Successful in 4s Details
/ mirror_to_github (push) Successful in 19s Details

This commit is contained in:
Leon van Kammen 2025-01-16 15:34:38 +01:00
parent c0405e344c
commit 65e38dc898
11 changed files with 328 additions and 295 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
git remote | grep codeberg || git remote add codeberg git@codeberg.org:xrsh/xrsh-com.git
git remote | grep c-frame || git remote add c-frame git@github.com:c-frame/xrsh-com.git

41
README.awk Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env -S awk -f
# a no-nonsense source-to-markdown generator which scans for:
#
# /**
# * # foo
# *
# * this is markdown $(cat bar.md)
# */
#
# var foo; // comment with 2 leading spaces is markdown too $(date)
#
# easily refactorable to hash-based languages (py/bash/perl/lua e.g.)
# by changing the regexes
#
BEGIN{
# printf README.md until '# Component List'
system("grep -B9999 '# Component List' README.md")
print ""
}
/\$\(/ { cmd=$0;
gsub(/^.*\$\(/,"",cmd);
gsub(/\).*/,"",cmd);
cmd | getline stdout; close(cmd);
sub(/\$\(.*\)/,stdout);
}
/\/\*\*/ { doc=1; sub(/^.*\/\*/,""); }
doc && /\*\// { doc=0;
sub(/[[:space:]]*\*\/.*/,"");
sub(/^[[:space:]]*\*[[:space:]]?/,"");
print
}
doc && /^[[:space:]]*\*/ { sub(/^[[:space:]]*\*[[:space:]]?/,"");
print
}
#!doc && /\/\/ / { sub(".*// ","");
# sub("# ","\n# ");
# sub("> ","\n> ");
# print
# }

149
README.md
View File

@ -17,6 +17,10 @@ Characteristics:
<a-entity helloworld="foo:1" class="cubes" name="box"> <a-entity helloworld="foo:1" class="cubes" name="box">
``` ```
See component list below
> this README.md is generated by running `echo "$(./README.awk com/*.js)" > README.md`
## Funding ## Funding
This project is partially funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/xrsh). This project is partially funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/xrsh).
@ -24,3 +28,148 @@ This project is partially funded through [NGI0 Entrust](https://nlnet.nl/entrust
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl) [<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/entrust) [<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/entrust)
# Component List
## [data_events](com/data_events.js)
allows components to react to data changes
```html
<script>
AFRAME.registerComponent('mycom',{
init: function(){ this.data.foo = 1 },
event: {
foo: (e) => alert("I was updated!")
}
})
</script>
<a-entity mycom data_events/>
```
## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
shows domid **only** in immersive mode
(wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
It also sets class 'XR' to the (HTML) body-element in immersive mode.
This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
> depends on [AFRAME.utils.require](com/require.js)
```html
<style type="text/css">
.XR #foo { color:red; }
</style>
<a-entity html-as-texture-in-xr="domid: #foo">
<b id="foo">hello</b>
</a-entitiy>
```
| property | type |
|--------------|--------------------|
| `domid` | `string` |
| event | target | info |
|--------------|------------|--------------------------------------|
| `3D` | a-scene | fired when going into immersive mode |
| `2D` | a-scene | fired when leaving immersive mode |
## [isoterminal](com/isoterminal.js)
Renders a windowed terminal in both (non)immersive mode.
It displays an interactive javascript console or boots into
a Linux ISO image (via WASM).
```html
<a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
```
> depends on [AFRAME.utils.require](com/require.js)
| property | type | default | info |
|------------------|-----------|------------------------|------|
| `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
| `overlayfs` | `string` | *WORK-IN-PROGRESS* | |
| `width` | `number` | 800 ||
| `height` | `number` | 600 ||
| `depth` | `number` | 0.03 ||
| `lineHeight` | `number` | 18 ||
| `prompt` | `boolean` | true | boot straight into ISO or give user choice |
| `padding` | `number`` | 18 | |
| `maximized` | `boolean` | false | |
| `minimized` | `boolean` | false | |
| `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO |
| `HUD` | `boolean` | false | link to camera movement |
| `transparent` | `boolean` | false | heavy, needs good gpu |
| `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
| `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
| `debug` | `boolean` | false | |
| `emulator` | `string` | fbterm | terminal emulator |
> for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
Component design:
```
css/html template
┌─────────┐ ┌────────────┐ exit-AR
┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐
│ └─────────┘ └───────────┬┘ │
│ │ │
┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐
│ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │
└────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘
│ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │
│ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── enter-VR │
│ └────────┘ └────────────────┘ enter-AR ◄─┘
│ │
│ │
│ ISOTerminal.js
│ ┌───────────────────────────┐
│ │ com/isoterminal/worker.js ├
│ └──────────────┌────────────┤
│ │ │ v86.js │
│ │ │ feat/*.js │
│ │ │ libv86.js │
│ │ └────────────┘
│ │
└─────────────────────┘
NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
```
## [pastedrop](com/pastedrop.js)
detects user copy/paste and file dragdrop action
and clipboard functions
```html
<a-entity pastedrop/>
```
| event | target | info |
|--------------|--------|------------------------------------------|
| `pasteFile` | self | always translates input to a File object |
## [require](com/require('').js)
automatically requires dependencies
```javascript
await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
```
> (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)

View File

@ -1,5 +1,5 @@
/* /**
* ## data_events * ## [data_events](com/data_events.js)
* *
* allows components to react to data changes * allows components to react to data changes
* *

View File

@ -1,5 +1,5 @@
/* /*
* ## dom * ## [dom](com/dom.js)
* *
* instances reactive DOM component from AFRAME component's `dom` metadata * instances reactive DOM component from AFRAME component's `dom` metadata
* *
@ -36,10 +36,6 @@ if( !AFRAME.components.dom ){
AFRAME.registerComponent('dom',{ AFRAME.registerComponent('dom',{
requires: {
"requestAnimationFrameXR": "com/requestAnimationFrameXR.js"
},
init: function(){ init: function(){
Object.values(this.el.components) Object.values(this.el.components)
.map( (c) => { .map( (c) => {

View File

@ -1,3 +1,35 @@
/**
* ## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
*
* shows domid **only** in immersive mode
* (wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
*
* It also sets class 'XR' to the (HTML) body-element in immersive mode.
* This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
*
* > depends on [AFRAME.utils.require](com/require.js)
*
* ```html
* <style type="text/css">
* .XR #foo { color:red; }
* </style>
*
* <a-entity html-as-texture-in-xr="domid: #foo">
* <b id="foo">hello</b>
* </a-entitiy>
* ```
*
* | property | type |
* |--------------|--------------------|
* | `domid` | `string` |
*
* | event | target | info |
* |--------------|------------|--------------------------------------|
* | `3D` | a-scene | fired when going into immersive mode |
* | `2D` | a-scene | fired when leaving immersive mode |
*
*/
if( !AFRAME.components['html-as-texture-in-xr'] ){ if( !AFRAME.components['html-as-texture-in-xr'] ){
AFRAME.registerComponent('html-as-texture-in-xr', { AFRAME.registerComponent('html-as-texture-in-xr', {

View File

@ -1,5 +1,40 @@
/* /**
* ## [isoterminal](com/isoterminal.js)
* *
* Renders a windowed terminal in both (non)immersive mode.
* It displays an interactive javascript console or boots into
* a Linux ISO image (via WASM).
*
* ```html
* <a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
* ```
*
* > depends on [AFRAME.utils.require](com/require.js)
*
* | property | type | default | info |
* |------------------|-----------|------------------------|------|
* | `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
* | `overlayfs` | `string` | *WORK-IN-PROGRESS* | |
* | `width` | `number` | 800 ||
* | `height` | `number` | 600 ||
* | `depth` | `number` | 0.03 ||
* | `lineHeight` | `number` | 18 ||
* | `prompt` | `boolean` | true | boot straight into ISO or give user choice |
* | `padding` | `number`` | 18 | |
* | `maximized` | `boolean` | false | |
* | `minimized` | `boolean` | false | |
* | `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO |
* | `HUD` | `boolean` | false | link to camera movement |
* | `transparent` | `boolean` | false | heavy, needs good gpu |
* | `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
* | `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
* | `debug` | `boolean` | false | |
* | `emulator` | `string` | fbterm | terminal emulator |
*
* > for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
*
* Component design:
* ```
* css/html template * css/html template
* *
* exit-AR * exit-AR
@ -27,6 +62,7 @@
* *
* NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal * NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
* Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit()) * Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
* ```
*/ */
if( typeof AFRAME != 'undefined '){ if( typeof AFRAME != 'undefined '){

View File

@ -1,3 +1,18 @@
/**
* ## [pastedrop](com/pastedrop.js)
*
* detects user copy/paste and file dragdrop action
* and clipboard functions
*
* ```html
* <a-entity pastedrop/>
* ```
*
* | event | target | info |
* |--------------|--------|------------------------------------------|
* | `pasteFile` | self | always translates input to a File object |
*/
AFRAME.registerComponent('pastedrop', { AFRAME.registerComponent('pastedrop', {
schema: { schema: {
foo: { type:"string"} foo: { type:"string"}
@ -22,16 +37,16 @@ AFRAME.registerComponent('pastedrop', {
}) })
}, },
//getClipboard: function(){ getClipboard: function(){
// navigator.clipboard.readText() navigator.clipboard.readText()
// .then( async (base64) => { .then( async (base64) => {
// let mimetype = base64.replace(/;base64,.*/,'') let mimetype = base64.replace(/;base64,.*/,'')
// let data = base64.replace(/.*;base64,/,'') let data = base64.replace(/.*;base64,/,'')
// let type = this.textHeuristic(data) let type = this.textHeuristic(data)
// const term = document.querySelector('[isoterminal]').components.isoterminal.term const term = document.querySelector('[isoterminal]').components.isoterminal.term
// this.el.emit('pasteFile',{}) /*TODO* data incompatible */ this.el.emit('pasteFile',{}) /*TODO* data incompatible */
// }) })
//}, },
onDrop: function(e){ onDrop: function(e){
e.preventDefault() e.preventDefault()

View File

@ -1,12 +1,17 @@
// usage: /**
// * ## [require](com/require('').js)
// await AFRAME.utils.require( this.dependencies ) (*) autoload missing components *
// await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components * automatically requires dependencies
// await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this) *
// await AFRAME.utils.require(["./app/foo.js","foo.css"],this) * ```javascript
// * await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
// (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.) * await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
// * await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
* await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
* ```
*
* > (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
*/
AFRAME.utils.require = function(arr_or_obj,opts){ AFRAME.utils.require = function(arr_or_obj,opts){
opts = opts || {} opts = opts || {}
let i = 0 let i = 0

View File

@ -1,3 +1,28 @@
* ## [window](com/window.js)
*
* wraps a draggable window around a dom id or [dom](com/dom.js) component.
*
* ```html
* <a-entity window="dom: #mydiv"/>
* ```
*
* > depends on [AFRAME.utils.require](com/require.js)
*
* | property | type | default | info |
* |------------------|-----------|------------------------|------|
* | `title` |`string` | "" | |
* | `width` |`string` | | |
* | `height` |`string` | 260px | |
* | `uid` |`string` | | |
* | `attach` |`selector` | | |
* | `dom` |`selector` | | |
* | `max` |`boolean` | false | |
* | `min` |`boolean` | false | |
* | `x` |`string` | "center" | |
* | `y` |`string` | "center" | |
* | `class` |`array` | [] | |
*/
AFRAME.registerComponent('window', { AFRAME.registerComponent('window', {
schema:{ schema:{
title: {type:'string',"default":"title"}, title: {type:'string',"default":"title"},

View File

@ -1,268 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2019
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* 2019 Mauve Ranger
* 2024 Leon van Kammen
*/
let terminalInstance = 0
const TERMINAL_THEME = {
theme_foreground: {
// 'default': '#ffffff'
},
theme_background: {
// 'default': '#000'
},
theme_cursor: {
// 'default': '#ffffff'
},
theme_selection: {
// 'default': 'rgba(255, 255, 255, 0.3)'
},
theme_black: {
// 'default': '#000000'
},
theme_red: {
// 'default': '#e06c75'
},
theme_brightRed: {
// 'default': '#e06c75'
},
theme_green: {
// 'default': '#A4EFA1'
},
theme_brightGreen: {
// 'default': '#A4EFA1'
},
theme_brightYellow: {
// 'default': '#EDDC96'
},
theme_yellow: {
// 'default': '#EDDC96'
},
theme_magenta: {
// 'default': '#e39ef7'
},
theme_brightMagenta: {
// 'default': '#e39ef7'
},
theme_cyan: {
// 'default': '#5fcbd8'
},
theme_brightBlue: {
// 'default': '#5fcbd8'
},
theme_brightCyan: {
// 'default': '#5fcbd8'
},
theme_blue: {
// 'default': '#5fcbd8'
},
theme_white: {
// 'default': '#d0d0d0'
},
theme_brightBlack: {
// 'default': '#808080'
},
theme_brightWhite: {
// 'default': '#ffffff'
}
}
AFRAME.registerComponent('xterm', {
schema: Object.assign({
XRrenderer: { type: 'string', default: 'canvas', },
cols: { type: 'number', default: 110, },
rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
canvasLatency:{ type:'number', default: 200 }
}, TERMINAL_THEME),
write: function(message) {
this.term.write(message)
},
init: function () {
const terminalElement = document.createElement('div')
terminalElement.setAttribute('style', `
width: 800px;
height: ${Math.floor( 800 * 0.527 )}px;
overflow: hidden;
`)
this.el.terminalElement = terminalElement
if( this.data.XRrenderer == 'canvas' ){
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
// and terminal text (this.el.planeText.getObject("mesh"))
const w = 2;
const h = (this.data.rows*5/this.data.cols)
this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`)
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
this.el.planeText = document.createElement('a-entity')
this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`)
this.el.appendChild(this.el.planeText)
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
}
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
if( this.el.sceneEl.renderer.xr.isPresenting ){
// workaround
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
//this.term._core.viewport._innerRefresh()
this.term._core.renderer._renderDebouncer._innerRefresh()
}
}, this.data.canvasLatency)
// Build up a theme object
const theme = Object.keys(this.data).reduce((theme, key) => {
if (!key.startsWith('theme_')) return theme
const data = this.data[key]
if(!data) return theme
theme[key.slice('theme_'.length)] = data
return theme
}, {})
this.fontSize = 14
const term = this.term = new Terminal({
logLevel:"off",
theme: theme,
allowTransparency: true,
cursorBlink: true,
disableStdin: false,
rows: this.data.rows,
cols: this.data.cols,
fontFamily: 'Cousine, monospace',
fontSize: this.fontSize,
lineHeight: 1.15,
useFlowControl: true,
rendererType: this.renderType // 'dom' // 'canvas'
})
this.term.open(terminalElement)
this.term.focus()
this.setRenderType('dom')
terminalElement.querySelector('.xterm-viewport').style.background = 'transparent'
// now we can scale canvases to the parent element
const $screen = terminalElement.querySelector('.xterm-screen')
$screen.style.width = '100%'
term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) )
term.on('data', (data) => {
this.el.emit('xterm-input', data)
})
this.el.addEventListener('serial-output-byte', (e) => {
const byte = e.detail
var chr = String.fromCharCode(byte);
this.term.write(chr)
})
this.el.addEventListener('serial-output-string', (e) => {
this.term.write(e.detail)
})
},
update: function(){
if( this.renderType == 'canvas' ){
const material = this.el.planeText.getObject3D('mesh').material
if (!material.map ) return
if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0)
else console.log("no cursorCanvas")
material.map.needsUpdate = true
//material.needsUpdate = true
}
},
setRenderType: function(type){
if( type.match(/(dom|canvas)/) ){
if( type == 'dom'){
this.el.dom.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', this.fontSize )
this.term.setOption('rendererType',type )
this.renderType = type
}
if( type == 'canvas'){
this.el.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', this.fontSize * 3 )
this.term.setOption('rendererType',type )
this.renderType = type
this.update()
setTimeout( () => {
this.canvas = this.el.terminalElement.querySelector('.xterm-text-layer')
this.canvas.id = "xterm-canvas"
this.canvasContext = this.canvas.getContext('2d')
this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer')
// Create a texture from the canvas
const canvasTexture = new THREE.Texture(this.canvas)
//canvasTexture.minFilter = THREE.NearestFilter //LinearFilter
//canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter
canvasTexture.needsUpdate = true; // Ensure the texture updates
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
if( plane.material ) plane.material.dispose()
plane.material = new THREE.MeshBasicMaterial({
map: canvasTexture, // Set the texture from the canvas
transparent: false, // Set transparency
//side: THREE.DoubleSide // Set to double-sided rendering
//blending: THREE.AdditiveBlending
});
this.el.object3D.scale.x = 0.2
this.el.object3D.scale.y = 0.2
this.el.object3D.scale.z = 0.2
},100)
}
this.el.terminalElement.style.opacity = type == 'canvas' ? 0 : 1
}
},
enterImmersive: function(){
if( this.mode == 'immersive' ) return
this.el.object3D.visible = true
this.mode = "immersive"
this.setRenderType('canvas')
this.term.focus()
},
exitImmersive: function(){
if( this.mode == 'nonimmersive' ) return
this.el.object3D.visible = false
this.mode = "nonimmersive"
this.setRenderType('dom')
},
})