launcher/handmenu demo
This commit is contained in:
		
							parent
							
								
									46bb9afe6d
								
							
						
					
					
						commit
						987828b233
					
				
					 26 changed files with 1227 additions and 835 deletions
				
			
		| 
						 | 
				
			
			@ -19,7 +19,7 @@ BEGIN{
 | 
			
		|||
  print ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/\$\(/                   { cmd=$0; 
 | 
			
		||||
/\$(?![\(|"])/           { cmd=$0; 
 | 
			
		||||
                           gsub(/^.*\$\(/,"",cmd); 
 | 
			
		||||
                           gsub(/\).*/,"",cmd);
 | 
			
		||||
                           cmd | getline stdout; close(cmd);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										80
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										80
									
								
								README.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -101,7 +101,7 @@ a Linux ISO image (via WASM).
 | 
			
		|||
| `height`         | `number`  |  600   ||
 | 
			
		||||
| `depth`          | `number`  |  0.03  ||
 | 
			
		||||
| `lineHeight`     | `number`  |  18    ||
 | 
			
		||||
| `prompt`         | `boolean` |  true  | boot straight into ISO or give user choice |
 | 
			
		||||
| `bootmenu`       | `boolean` |  true  | give user choice [or boot straight into ISO ] |
 | 
			
		||||
| `padding`        | `number`` |  18    | |
 | 
			
		||||
| `maximized`      | `boolean` | false  | |
 | 
			
		||||
| `minimized`      | `boolean` | false  | |
 | 
			
		||||
| 
						 | 
				
			
			@ -147,6 +147,54 @@ NOTE: For convenience reasons, events are forwarded between com/isoterminal.js,
 | 
			
		|||
```       
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [launcher](com/launcher.js)
 | 
			
		||||
 | 
			
		||||
displays app (icons) for enduser to launch
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
 <a-scene launcher/>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
| property     | type               | example                                                                                |
 | 
			
		||||
|--------------|--------------------|----------------------------------------------------------------------------------------|
 | 
			
		||||
| `registries` | `array` of strings | `<a-entity launcher="registers: https://foo.com/index.json, ./index.json"/>`           |
 | 
			
		||||
 | 
			
		||||
| event        | target | info                                                                                               |
 | 
			
		||||
|--------------|-------------------------------------------------------------------------------------------------------------|
 | 
			
		||||
| `launcher`   | an app | when pressing an app icon, `launcher` event will be send to the respective app                     |
 | 
			
		||||
 | 
			
		||||
There a multiple ways of letting the launcher know that an app can be launched:
 | 
			
		||||
 | 
			
		||||
1. any AFRAME component with an `launcher`-event + manifest is automatically added:
 | 
			
		||||
 
 | 
			
		||||
 ```javascript
 | 
			
		||||
 AFRAME.registerComponent('foo',{
 | 
			
		||||
   events:{
 | 
			
		||||
     launcher: function(){ ...launch something... }
 | 
			
		||||
   },
 | 
			
		||||
   manifest:{ // HTML5 manifesto JSON object
 | 
			
		||||
     // https://www.w3.org/TR/appmanifest/ 
 | 
			
		||||
   }
 | 
			
		||||
 }
 | 
			
		||||
 ```
 | 
			
		||||
 | 
			
		||||
2. dynamically in javascript   
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
window.launcher.register({
 | 
			
		||||
  name:"foo",
 | 
			
		||||
  icon: "https://.../optional_icon.png" 
 | 
			
		||||
  description: "lorem ipsum",
 | 
			
		||||
  cb: () => alert("foo")
 | 
			
		||||
})
 | 
			
		||||
//window.launcher.unregister('foo')
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Restore the pose.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [pastedrop](com/pastedrop.js)
 | 
			
		||||
 | 
			
		||||
detects user copy/paste and file dragdrop action
 | 
			
		||||
| 
						 | 
				
			
			@ -161,9 +209,12 @@ and clipboard functions
 | 
			
		|||
| `pasteFile`  | self   | always translates input to a File object |
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Restore the pose.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [require](com/require('').js)
 | 
			
		||||
 | 
			
		||||
automatically requires dependencies 
 | 
			
		||||
automatically requires dependencies or missing components
 | 
			
		||||
 | 
			
		||||
```javascript
 | 
			
		||||
await AFRAME.utils.require( this.dependencies )               (*) autoload missing components
 | 
			
		||||
| 
						 | 
				
			
			@ -173,3 +224,28 @@ await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
 | 
			
		|||
```
 | 
			
		||||
 | 
			
		||||
> (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [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`    | []                     |      |
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
window.AFRAME.registerComponent('xrf-wear', {
 | 
			
		||||
  schema:{
 | 
			
		||||
    el: {type:"selector"}, 
 | 
			
		||||
    position: {type:"vec3"}, 
 | 
			
		||||
    rotation: {type:"vec3"} 
 | 
			
		||||
  }, 
 | 
			
		||||
  init: function(){
 | 
			
		||||
    $('a-scene').addEventListener('enter-vr', (e) => this.wear(e) )
 | 
			
		||||
    $('a-scene').addEventListener('exit-vr',  (e) => this.unwear(e) )
 | 
			
		||||
  }, 
 | 
			
		||||
  wear: function(){
 | 
			
		||||
    if( !this.wearable ){
 | 
			
		||||
      let d = this.data
 | 
			
		||||
      this.wearable = new THREE.Group()
 | 
			
		||||
      this.el.object3D.children.map( (c) => this.wearable.add(c) )
 | 
			
		||||
      this.wearable.position.set( d.position.x,  d.position.y,  d.position.z)
 | 
			
		||||
      this.wearable.rotation.set( d.rotation.x,  d.rotation.y,  d.rotation.z)
 | 
			
		||||
    }
 | 
			
		||||
    this.data.el.object3D.add(this.wearable)
 | 
			
		||||
  }, 
 | 
			
		||||
  unwear: function(){
 | 
			
		||||
    this.data.el.remove(this.wearable)
 | 
			
		||||
    this.wearable.children.map( (c) => this.el.object3D.add(c) )
 | 
			
		||||
    delete this.wearable
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										85
									
								
								com/controlattach.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								com/controlattach.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
/*
 | 
			
		||||
 *Usage:
 | 
			
		||||
 *        <a-entity  controlattach="el: #leftHand; class: foo"></a-entity>
 | 
			
		||||
 *        <a-entity id="leftHand">
 | 
			
		||||
 *          <a-entity class="foo" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
 | 
			
		||||
 *        <a-entity>
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: you can hint different position/rotation-offsets by adding controller-specific entities to the target:
 | 
			
		||||
 *
 | 
			
		||||
 *        <a-entity id="leftHand">
 | 
			
		||||
 *          <a-entity class="foo" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
 | 
			
		||||
 *          <a-entity class="foo hand-tracking-controls" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
 | 
			
		||||
 *        <a-entity>
 | 
			
		||||
 *
 | 
			
		||||
 * The controllernames are hinted by the 'controllermodelready' event:
 | 
			
		||||
 *
 | 
			
		||||
 *  'hand-tracking-controls',
 | 
			
		||||
 *  'meta-touch-controls',
 | 
			
		||||
 *  'valve-index-controls',
 | 
			
		||||
 *  'logitech-mx-ink-controls',
 | 
			
		||||
 *  'windows-motion-controls',
 | 
			
		||||
 *  'hp-mixed-reality-controls',
 | 
			
		||||
 *  'generic-tracked-controller-controls',
 | 
			
		||||
 *  'pico-controls',
 | 
			
		||||
 *  (and more in the future)
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
AFRAME.registerComponent('controlattach', {
 | 
			
		||||
  schema:{
 | 
			
		||||
    el: {type:"selector"},
 | 
			
		||||
    class: {type:"string"}
 | 
			
		||||
  }, 
 | 
			
		||||
  init: function(){
 | 
			
		||||
    if( !this.data.el ) return console.warn(`controlattach.js: cannot find ${this.data.el}`)
 | 
			
		||||
    this.controllers = {}
 | 
			
		||||
    this.remember()
 | 
			
		||||
    this.data.el.addEventListener('controllermodelready',  this.bindPlaceHolders.bind(this,["controllermodelready"]) )
 | 
			
		||||
    this.data.el.addEventListener('controllerconnected',   this.bindPlaceHolders.bind(this,["controllerconnected"] ) )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  bindPlaceHolders: function(type,e){
 | 
			
		||||
    let controllerName = e.detail.name
 | 
			
		||||
    let selector       =  `.${this.data.class}.${controllerName}`
 | 
			
		||||
    let placeholder    = this.data.el.querySelector(selector)
 | 
			
		||||
    if( !placeholder ){
 | 
			
		||||
      placeholder = this.data.el.querySelector(`.${this.data.class}`)
 | 
			
		||||
      console.warn(`controlattach.js: '${selector}' not found, fallback to default`)
 | 
			
		||||
    }
 | 
			
		||||
    if( !placeholder ) return console.warn("controlattach.js: could not find placeholder to attach to")
 | 
			
		||||
    this.attachPlaceHolder(type,e,placeholder, controllerName)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  attachPlaceHolder: function(type, el, placeholder, controllerName){
 | 
			
		||||
    this.el.object3DMap = {} // unsync THREE <-> AFRAME entity
 | 
			
		||||
    // these are handled by the placeholder entity
 | 
			
		||||
    this.obj.position.set(0,0,0);
 | 
			
		||||
    this.obj.rotation.set(0,0,0);
 | 
			
		||||
    this.obj.scale.set(1,1,1);
 | 
			
		||||
 | 
			
		||||
    if( controllerName != 'hand-tracking-controls' ){
 | 
			
		||||
      // re-add for controller-models which don't re-add children ('meta-touch-controls' e.g.)
 | 
			
		||||
      if( this.data.el.getObject3D("mesh") ){
 | 
			
		||||
        this.data.el.getObject3D("mesh").add(placeholder.object3D)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    placeholder.object3D.add( this.obj )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  detach: function(){
 | 
			
		||||
    this.el.setObject3D( this.obj.uuid, this.obj )
 | 
			
		||||
    this.el.object3D.position.copy( this.position )
 | 
			
		||||
    this.el.object3D.rotation.copy( this.rotation )
 | 
			
		||||
    this.el.object3D.scale.copy( this.scale )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  remember: function(){
 | 
			
		||||
    this.obj      = this.el.object3D
 | 
			
		||||
    this.position = this.el.object3D.position.clone()
 | 
			
		||||
    this.rotation = this.el.object3D.rotation.clone()
 | 
			
		||||
    this.scale    = this.el.object3D.scale.clone()
 | 
			
		||||
    this.parent   = this.el.object3D.parent
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -94,8 +94,12 @@ if( !AFRAME.components.dom ){
 | 
			
		|||
      this.el.dom.innerHTML = this.dom.html(this)
 | 
			
		||||
      this.el.dom.className = this.dom.attrName 
 | 
			
		||||
      this.com.data = this.reactify( this.el, this.com.data )
 | 
			
		||||
      if( this.dom.events ) this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
 | 
			
		||||
      this.el.dom = this.el.dom.children[0]
 | 
			
		||||
      if( this.dom.events ){ 
 | 
			
		||||
        this.dom.events.map( (e) => {
 | 
			
		||||
          this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev)  )
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      return this
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,107 +0,0 @@
 | 
			
		|||
AFRAME.registerComponent('helloworld-html', {
 | 
			
		||||
  schema: { 
 | 
			
		||||
    foo: { type:"string"}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  init: function () {  
 | 
			
		||||
 | 
			
		||||
    this.el.addEventListener('ready', () => this.el.dom.style.display = 'none' )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  requires:{
 | 
			
		||||
    html:        "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js",  // html to AFRAME
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dom: {
 | 
			
		||||
    scale:   1,
 | 
			
		||||
    events:  ['click'],
 | 
			
		||||
    html:    (me) => `<div>
 | 
			
		||||
                        <div class="pad"> helloworld-html: ${me.data.foo} <b>${me.data.myvalue}</b></span>
 | 
			
		||||
                      </div>`,
 | 
			
		||||
 | 
			
		||||
    css:     (me) => `.helloworld-html {
 | 
			
		||||
                color: var(--xrsh-light-gray); /* see index.css */
 | 
			
		||||
              }`,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  events:{
 | 
			
		||||
 | 
			
		||||
    // combined AFRAME+DOM reactive events
 | 
			
		||||
    keydown: function(e){ }, // 
 | 
			
		||||
    click:    function(e){ 
 | 
			
		||||
      console.dir(e)
 | 
			
		||||
    }, // 
 | 
			
		||||
 | 
			
		||||
    // reactive events for this.data updates 
 | 
			
		||||
    myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    ready: function( ){ 
 | 
			
		||||
      this.el.dom.style.display = 'none'
 | 
			
		||||
      console.log("this.el.dom has been added to DOM")
 | 
			
		||||
      this.el.dom.children[0].id = this.el.uid  // important hint for html-mesh
 | 
			
		||||
      this.data.myvalue = 1
 | 
			
		||||
      setInterval( () => this.data.myvalue++, 100 )
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    launcher:  function(){ 
 | 
			
		||||
      this.el.dom.style.display = ''
 | 
			
		||||
      console.log("launcher") 
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  manifest: { // HTML5 manifest to identify app to xrsh
 | 
			
		||||
    "short_name": "Hello world",
 | 
			
		||||
    "name": "Hello world",
 | 
			
		||||
    "icons": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "https://css.gg/browser.svg",
 | 
			
		||||
        "src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
 | 
			
		||||
        "type": "image/svg+xml",
 | 
			
		||||
        "sizes": "512x512"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "id": "/?source=pwa",
 | 
			
		||||
    "start_url": "/?source=pwa",
 | 
			
		||||
    "background_color": "#3367D6",
 | 
			
		||||
    "display": "standalone",
 | 
			
		||||
    "scope": "/",
 | 
			
		||||
    "theme_color": "#3367D6",
 | 
			
		||||
    "shortcuts": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "What is the latest news?",
 | 
			
		||||
        "cli":{
 | 
			
		||||
          "usage":  "helloworld <type> [options]",
 | 
			
		||||
          "example": "helloworld news",
 | 
			
		||||
          "args":{
 | 
			
		||||
            "--latest": {type:"string"}
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "short_name": "Today",
 | 
			
		||||
        "description": "View weather information for today",
 | 
			
		||||
        "url": "/today?source=pwa",
 | 
			
		||||
        "icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "description": "Hello world information",
 | 
			
		||||
    "screenshots": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "/images/screenshot1.png",
 | 
			
		||||
        "type": "image/png",
 | 
			
		||||
        "sizes": "540x720",
 | 
			
		||||
        "form_factor": "narrow"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "help":`
 | 
			
		||||
Helloworld application 
 | 
			
		||||
 | 
			
		||||
This is a help file which describes the application.
 | 
			
		||||
It will be rendered thru troika text, and will contain
 | 
			
		||||
headers based on non-punctualized lines separated by linebreaks,
 | 
			
		||||
in above's case "\nHelloworld application\n" will qualify as header.
 | 
			
		||||
    `
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,156 +0,0 @@
 | 
			
		|||
AFRAME.registerComponent('helloworld-iframe', {
 | 
			
		||||
  schema: { 
 | 
			
		||||
    url: { type:"string"}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  init: function(){},
 | 
			
		||||
 | 
			
		||||
  requires:{
 | 
			
		||||
    html:        "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js",  // html to AFRAME
 | 
			
		||||
    winboxjs:    "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js",     // deadsimple windows: https://nextapps-de.github.io/winbox
 | 
			
		||||
    winboxcss:   "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css",       // 
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dom: {
 | 
			
		||||
    scale:   3,
 | 
			
		||||
    events:  ['click','keydown'],
 | 
			
		||||
    html:    (me) => `<div class="relative">
 | 
			
		||||
                        <span id="warning">
 | 
			
		||||
                          <b>Unfortunately,</b><br><br>
 | 
			
		||||
                          The browser does not allow IFRAME rendering<br>
 | 
			
		||||
                          in immersive mode (for security reasons).<br><br>
 | 
			
		||||
                        </span>  
 | 
			
		||||
                        <iframe src=""></iframe>
 | 
			
		||||
                      </div>`,
 | 
			
		||||
 | 
			
		||||
    css:     (me) => `
 | 
			
		||||
      .XR #overlay .winbox.iframe{ 
 | 
			
		||||
        visibility: visible; 
 | 
			
		||||
        position:relative;
 | 
			
		||||
      } /* don't hide in XR mode */
 | 
			
		||||
      .winbox.iframe > .wb-body  { background:#FFF !important; 
 | 
			
		||||
                                   overflow-y: hidden;
 | 
			
		||||
                                   overflow-x: hidden;
 | 
			
		||||
                                 }
 | 
			
		||||
      .winbox.iframe iframe      { z-index:10;    }
 | 
			
		||||
      .winbox.iframe #warning    { position:absolute; z-index:9; top:100px; left:20px; width:100%; height:50px; color:black; display:none; }
 | 
			
		||||
    `
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  events:{
 | 
			
		||||
 | 
			
		||||
    // combined AFRAME+DOM reactive events
 | 
			
		||||
    click:   function(e){ }, // 
 | 
			
		||||
    keydown: function(e){ }, // 
 | 
			
		||||
 | 
			
		||||
    // reactive updates (data2event.js)
 | 
			
		||||
    url: function(e){ 
 | 
			
		||||
      this.el.dom.querySelector('iframe').src = this.data.url
 | 
			
		||||
      console.dir(this.el.dom.querySelector('iframe'))
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    launcher: async function(){
 | 
			
		||||
      let URL = this.data.url || prompt('enter URL to display','https://fabien.benetou.fr/Wiki/Wiki')
 | 
			
		||||
      if( !URL ) return
 | 
			
		||||
 | 
			
		||||
      this.createWindow()
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    createWindow: function(){
 | 
			
		||||
      let s = await AFRAME.utils.require(this.requires)
 | 
			
		||||
 | 
			
		||||
      // instance this component
 | 
			
		||||
      const instance = this.el.cloneNode(false)
 | 
			
		||||
      this.el.sceneEl.appendChild( instance )
 | 
			
		||||
      instance.setAttribute("dom",       "")
 | 
			
		||||
      instance.setAttribute("data2event","")
 | 
			
		||||
      instance.setAttribute("visible",  AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
 | 
			
		||||
      instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(1.39) )
 | 
			
		||||
      instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
 | 
			
		||||
 | 
			
		||||
      this.el.sceneEl.addEventListener('3D', () => {
 | 
			
		||||
        instance.dom.querySelector('#warning').style.display = 'block' // show warning
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const setupWindow = () => {
 | 
			
		||||
        const com = instance.components['helloworld-iframe']
 | 
			
		||||
        instance.dom.style.display = 'none'
 | 
			
		||||
        new WinBox( this.manifest.short_name+" "+URL,{ 
 | 
			
		||||
          width: Math.round(window.innerWidth*0.6),
 | 
			
		||||
          height: Math.round(window.innerHeight*0.6),
 | 
			
		||||
          class:["iframe"],
 | 
			
		||||
          x:"center",
 | 
			
		||||
          y:"center",
 | 
			
		||||
          id:  instance.uid, // important hint for html-mesh  
 | 
			
		||||
          root: document.querySelector("#overlay"),
 | 
			
		||||
          mount: instance.dom,
 | 
			
		||||
          onclose: () => { instance.dom.style.display = 'none'; return false; },
 | 
			
		||||
          oncreate: () => {
 | 
			
		||||
            com.data.url = URL
 | 
			
		||||
            instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
 | 
			
		||||
            instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
 | 
			
		||||
            instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        instance.dom.style.display = ''
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setTimeout( () => setupWindow(), 10 ) // give new components time to init
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  manifest: { // HTML5 manifest to identify app to xrsh
 | 
			
		||||
    "short_name": "Iframe",
 | 
			
		||||
    "name": "Hello world IFRAME window",
 | 
			
		||||
    "icons": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "https://css.gg/browse.svg",
 | 
			
		||||
        "src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
 | 
			
		||||
        "type": "image/svg+xml",
 | 
			
		||||
        "sizes": "512x512"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "id": "/?source=pwa",
 | 
			
		||||
    "start_url": "/?source=pwa",
 | 
			
		||||
    "background_color": "#3367D6",
 | 
			
		||||
    "display": "standalone",
 | 
			
		||||
    "scope": "/",
 | 
			
		||||
    "theme_color": "#3367D6",
 | 
			
		||||
    "shortcuts": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "What is the latest news?",
 | 
			
		||||
        "cli":{
 | 
			
		||||
          "usage":  "helloworld <type> [options]",
 | 
			
		||||
          "example": "helloworld news",
 | 
			
		||||
          "args":{
 | 
			
		||||
            "--latest": {type:"string"}
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "short_name": "Today",
 | 
			
		||||
        "description": "View weather information for today",
 | 
			
		||||
        "url": "/today?source=pwa",
 | 
			
		||||
        "icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "description": "Hello world information",
 | 
			
		||||
    "screenshots": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "/images/screenshot1.png",
 | 
			
		||||
        "type": "image/png",
 | 
			
		||||
        "sizes": "540x720",
 | 
			
		||||
        "form_factor": "narrow"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "help":`
 | 
			
		||||
Helloworld application 
 | 
			
		||||
 | 
			
		||||
This is a help file which describes the application.
 | 
			
		||||
It will be rendered thru troika text, and will contain
 | 
			
		||||
headers based on non-punctualized lines separated by linebreaks,
 | 
			
		||||
in above's case "\nHelloworld application\n" will qualify as header.
 | 
			
		||||
    `
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,135 +0,0 @@
 | 
			
		|||
AFRAME.registerComponent('helloworld-window', {
 | 
			
		||||
  schema: { 
 | 
			
		||||
    foo: { type:"string", "default":"foo"}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  requires:     {
 | 
			
		||||
    dom:         "./com/dom.js",                                                  // interpret .dom object
 | 
			
		||||
    xrtexture:   "./com/html-as-texture-in-xr.js",                                // allow switching between 3D/3D 
 | 
			
		||||
    html:        "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js",  // html to AFRAME
 | 
			
		||||
    winboxjs:    "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js",     // deadsimple windows: https://nextapps-de.github.io/winbox
 | 
			
		||||
    winboxcss:   "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css",       // 
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  init: function(){ },
 | 
			
		||||
 | 
			
		||||
  dom: {
 | 
			
		||||
    scale:   0.8,
 | 
			
		||||
    events:  ['click','keydown'],
 | 
			
		||||
    html:    (me) => `<div>
 | 
			
		||||
                        <div class="pad"> <span>${me.data.foo}</span> <b>${me.data.myvalue}</b></span>
 | 
			
		||||
                      </div>`,
 | 
			
		||||
 | 
			
		||||
    css:    (me) =>  `.helloworld-window div.pad { padding:11px; }`
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  events:{
 | 
			
		||||
 | 
			
		||||
    // combined AFRAME+DOM reactive events
 | 
			
		||||
    click:   function(e){ }, // 
 | 
			
		||||
    keydown: function(e){ }, // 
 | 
			
		||||
 | 
			
		||||
    // reactive events for this.data updates (data2event.js)
 | 
			
		||||
    myvalue: function(e){ this.el.dom.querySelector('b').innerText    = this.data.myvalue },
 | 
			
		||||
    foo:     function(e){ this.el.dom.querySelector('span').innerText = this.data.foo },
 | 
			
		||||
 | 
			
		||||
    launcher: async function(){
 | 
			
		||||
      let s = await AFRAME.utils.require(this.requires)
 | 
			
		||||
 | 
			
		||||
      // instance this component
 | 
			
		||||
      const instance = this.el.cloneNode(false)
 | 
			
		||||
      this.el.sceneEl.appendChild( instance )
 | 
			
		||||
      instance.setAttribute("dom",             "")
 | 
			
		||||
      instance.setAttribute("grabbable","")
 | 
			
		||||
      instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
 | 
			
		||||
 | 
			
		||||
      const setupWindow = () => {
 | 
			
		||||
        const com = instance.components['helloworld-window']
 | 
			
		||||
        instance.dom.style.display = 'none'
 | 
			
		||||
 | 
			
		||||
        new WinBox("Hello World",{ 
 | 
			
		||||
          width: 250,
 | 
			
		||||
          height: 150,
 | 
			
		||||
          x:"center",
 | 
			
		||||
          y:"center",
 | 
			
		||||
          id:  instance.uid, // important hint for html-mesh  
 | 
			
		||||
          root: document.querySelector("#overlay"),
 | 
			
		||||
          mount: instance.dom,
 | 
			
		||||
          onclose: () => { instance.dom.style.display = 'none'; return false; },
 | 
			
		||||
          oncreate: () => {
 | 
			
		||||
            instance.setAttribute("show-texture-in-xr",`domid: #${instance.uid}`) // only show aframe-html texture in xr mode
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        instance.dom.style.display = '' // show
 | 
			
		||||
 | 
			
		||||
        // hint grabbable's obb-collider to track the window-object 
 | 
			
		||||
        instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
 | 
			
		||||
        instance.components['obb-collider'].update() 
 | 
			
		||||
 | 
			
		||||
        // data2event demo
 | 
			
		||||
        instance.setAttribute("data2event","")
 | 
			
		||||
        com.data.myvalue = 1
 | 
			
		||||
        com.data.foo     = `instance ${instance.uid}: `
 | 
			
		||||
        setInterval( () => com.data.myvalue++, 500 )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setTimeout( () => setupWindow(), 10 ) // give new components time to init
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  manifest: { // HTML5 manifest to identify app to xrsh
 | 
			
		||||
    "short_name": "window",
 | 
			
		||||
    "name": "Hello world window",
 | 
			
		||||
    "icons": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "https://css.gg/browser.svg",
 | 
			
		||||
        "src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
 | 
			
		||||
        "type": "image/svg+xml",
 | 
			
		||||
        "sizes": "512x512"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "id": "/?source=pwa",
 | 
			
		||||
    "start_url": "/?source=pwa",
 | 
			
		||||
    "background_color": "#3367D6",
 | 
			
		||||
    "display": "standalone",
 | 
			
		||||
    "scope": "/",
 | 
			
		||||
    "theme_color": "#3367D6",
 | 
			
		||||
    "shortcuts": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "What is the latest news?",
 | 
			
		||||
        "cli":{
 | 
			
		||||
          "usage":  "helloworld <type> [options]",
 | 
			
		||||
          "example": "helloworld news",
 | 
			
		||||
          "args":{
 | 
			
		||||
            "--latest": {type:"string"}
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "short_name": "Today",
 | 
			
		||||
        "description": "View weather information for today",
 | 
			
		||||
        "url": "/today?source=pwa",
 | 
			
		||||
        "icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "description": "Hello world information",
 | 
			
		||||
    "screenshots": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "/images/screenshot1.png",
 | 
			
		||||
        "type": "image/png",
 | 
			
		||||
        "sizes": "540x720",
 | 
			
		||||
        "form_factor": "narrow"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "help":`
 | 
			
		||||
Helloworld application 
 | 
			
		||||
 | 
			
		||||
This is a help file which describes the application.
 | 
			
		||||
It will be rendered thru troika text, and will contain
 | 
			
		||||
headers based on non-punctualized lines separated by linebreaks,
 | 
			
		||||
in above's case "\nHelloworld application\n" will qualify as header.
 | 
			
		||||
    `
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,94 +0,0 @@
 | 
			
		|||
AFRAME.registerComponent('helloworld', {
 | 
			
		||||
  schema: { 
 | 
			
		||||
    wireframe: { type:"boolean", "default":false},
 | 
			
		||||
    text: {type:"string","default":"hello world"}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dependencies: ['data2event'],
 | 
			
		||||
 | 
			
		||||
  init: async function() {
 | 
			
		||||
    this.el.object3D.visible = false
 | 
			
		||||
 | 
			
		||||
    await AFRAME.utils.require(this.dependencies)
 | 
			
		||||
    this.el.setAttribute("data2event","")
 | 
			
		||||
    this.el.setAttribute("grabbable","")
 | 
			
		||||
 | 
			
		||||
    this.el.innerHTML = `
 | 
			
		||||
       <a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
 | 
			
		||||
       <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
 | 
			
		||||
       <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
 | 
			
		||||
       <a-entity position="0 1.8 -3" scale="10 10 10" text="value: ${this.data.text}; align:center; color:#888"></a-entity>
 | 
			
		||||
    `
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  events:{
 | 
			
		||||
 | 
			
		||||
    launcher:      function(e){ 
 | 
			
		||||
      this.el.object3D.visible = !this.el.object3D.visible 
 | 
			
		||||
      clearInterval(this.interval)
 | 
			
		||||
      this.interval            = setInterval( () => {
 | 
			
		||||
        this.data.wireframe = !this.data.wireframe
 | 
			
		||||
      }, 500  )
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // reactive this.data value demo 
 | 
			
		||||
    wireframe:function( ){ 
 | 
			
		||||
      this.el.object3D.traverse( (obj) => obj.material && (obj.material.wireframe = this.data.wireframe) )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  manifest: { // HTML5 manifest to identify app to xrsh
 | 
			
		||||
    "short_name": "Hello world",
 | 
			
		||||
    "name": "Hello world",
 | 
			
		||||
    "icons": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "https://css.gg/shape-hexagon.svg",
 | 
			
		||||
        "src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNNiAxNS4yMzQ4TDEyIDE4LjU2ODFMMTggMTUuMjM0OFY4Ljc2NTIxTDEyIDUuNDMxODhMNiA4Ljc2NTIxVjE1LjIzNDhaTTEyIDJMMyA3VjE3TDEyIDIyTDIxIDE3VjdMMTIgMloiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
 | 
			
		||||
        "type": "image/svg+xml",
 | 
			
		||||
        "sizes": "512x512"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "id": "/?source=pwa",
 | 
			
		||||
    "start_url": "/?source=pwa",
 | 
			
		||||
    "background_color": "#3367D6",
 | 
			
		||||
    "display": "standalone",
 | 
			
		||||
    "scope": "/",
 | 
			
		||||
    "theme_color": "#3367D6",
 | 
			
		||||
    "shortcuts": [
 | 
			
		||||
      {
 | 
			
		||||
        "name": "What is the latest news?",
 | 
			
		||||
        "cli":{
 | 
			
		||||
          "usage":  "helloworld <type> [options]",
 | 
			
		||||
          "example": "helloworld news",
 | 
			
		||||
          "args":{
 | 
			
		||||
            "--latest": {type:"string"}
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "short_name": "Today",
 | 
			
		||||
        "description": "View weather information for today",
 | 
			
		||||
        "url": "/today?source=pwa",
 | 
			
		||||
        "icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "description": "Hello world information",
 | 
			
		||||
    "screenshots": [
 | 
			
		||||
      {
 | 
			
		||||
        "src": "/images/screenshot1.png",
 | 
			
		||||
        "type": "image/png",
 | 
			
		||||
        "sizes": "540x720",
 | 
			
		||||
        "form_factor": "narrow"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "help":`
 | 
			
		||||
Helloworld application 
 | 
			
		||||
 | 
			
		||||
This is a help file which describes the application.
 | 
			
		||||
It will be rendered thru troika text, and will contain
 | 
			
		||||
headers based on non-punctualized lines separated by linebreaks,
 | 
			
		||||
in above's case "\nHelloworld application\n" will qualify as header.
 | 
			
		||||
    `
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1,34 +1,44 @@
 | 
			
		|||
AFRAME.registerComponent('helloworld-htmlform', {
 | 
			
		||||
AFRAME.registerComponent('helloworld-window', {
 | 
			
		||||
  schema: { 
 | 
			
		||||
    foo: { type:"string"}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  init: function () {},
 | 
			
		||||
  init: function () {
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  requires:{
 | 
			
		||||
    html:        "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js",  // html to AFRAME
 | 
			
		||||
    winboxjs:    "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js",     // deadsimple windows: https://nextapps-de.github.io/winbox
 | 
			
		||||
    winboxcss:   "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css",       // deadsimple windows: https://nextapps-de.github.io/winbox
 | 
			
		||||
    window:   "com/window.js",
 | 
			
		||||
    reactive: "com/data2event.js"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dom: {
 | 
			
		||||
    scale:   1,
 | 
			
		||||
    scale:   0.66,
 | 
			
		||||
    events:  ['click','input'],
 | 
			
		||||
    html:    (me) => `<div class="htmlform">
 | 
			
		||||
                        <fieldset>
 | 
			
		||||
                          <legend>Colour</legend>
 | 
			
		||||
                          <input type="radio" id="color-red" name="color" value="red" checked><label for="color-red"> Red</label><br>
 | 
			
		||||
                          <input type="radio" id="color-blue" name="color" value="blue"><label for="color-blue"> Blue</label><br>
 | 
			
		||||
                          <legend>Theme</legend>
 | 
			
		||||
                          <input type="radio" id="theme" name="theme" value="0" checked style=""><label for="theme" style="margin-right:15px;">Normal</label>
 | 
			
		||||
                          <input type="radio" id="themei" name="theme" value="1"><label for="themei">Invert</label><br>
 | 
			
		||||
                        </fieldset>
 | 
			
		||||
                        <br>
 | 
			
		||||
                        <fieldset>
 | 
			
		||||
                          <legend>Material:</legend>
 | 
			
		||||
                          <input id="material-wireframe" type="checkbox" name="wireframe"><label for="material-wireframe"> Wireframe</label><br>
 | 
			
		||||
                          <legend>Welcome to XR Shell</legend>
 | 
			
		||||
                          A free offline-first morphable<br>
 | 
			
		||||
                          environment which provides <br>
 | 
			
		||||
                          <span id="myvalue"></span>  XR-friendly shells.<br>
 | 
			
		||||
                          <ol>
 | 
			
		||||
                            <li>check the <a href="/" target="_blank">website</a></li> 
 | 
			
		||||
                            <li>check the <a href="https://forgejo.isvery.ninja/xrsh/xrsh-buildroot/src/branch/main/buildroot-v86/board/v86/rootfs_overlay/root/manual.md" target="_blank">manual</a></li> 
 | 
			
		||||
                          </ol>
 | 
			
		||||
                        </fieldset>
 | 
			
		||||
                        <!--
 | 
			
		||||
                        <fieldset>
 | 
			
		||||
                          <legend>Size</legend>
 | 
			
		||||
                          <input type="range" min="0.1" max="2" value="1" step="0.01" id="myRange" style="background-color: transparent;">
 | 
			
		||||
                        </fieldset>
 | 
			
		||||
                        <button>hello <span id="myvalue"></span></button>
 | 
			
		||||
                        -->
 | 
			
		||||
                      </div>`,
 | 
			
		||||
 | 
			
		||||
    css:     (me) => `.htmlform { padding:11px; }`
 | 
			
		||||
| 
						 | 
				
			
			@ -38,13 +48,16 @@ AFRAME.registerComponent('helloworld-htmlform', {
 | 
			
		|||
  events:{
 | 
			
		||||
 | 
			
		||||
    // component events
 | 
			
		||||
    html:     function( ){ console.log("html-mesh requirement mounted") },
 | 
			
		||||
    window:     function( ){ console.log("window component mounted") },
 | 
			
		||||
 | 
			
		||||
    // combined AFRAME+DOM reactive events
 | 
			
		||||
    click: function(e){ }, // 
 | 
			
		||||
    click: function(e){ console.dir(e) }, // 
 | 
			
		||||
    input: function(e){
 | 
			
		||||
      if( !e.detail.target                 ) return
 | 
			
		||||
      if(  e.detail.target.id == 'myRange' ) this.data.myvalue = e.detail.target.value // reactive demonstration
 | 
			
		||||
      if(  e.detail.target.name == 'theme' ) document.body.style.filter = `invert(${e.detail.target.value})`
 | 
			
		||||
      if(  e.detail.target.name == 'cmenu' ) document.querySelector(".iconmenu").style.display = e.detail.target.value == 'on' ? '' : 'none';
 | 
			
		||||
      console.dir(e.detail)
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // reactive events for this.data updates 
 | 
			
		||||
| 
						 | 
				
			
			@ -54,51 +67,23 @@ AFRAME.registerComponent('helloworld-htmlform', {
 | 
			
		|||
      let s = await AFRAME.utils.require(this.requires)
 | 
			
		||||
 | 
			
		||||
      // instance this component
 | 
			
		||||
      const instance = this.el.cloneNode(false)
 | 
			
		||||
      this.el.sceneEl.appendChild( instance )
 | 
			
		||||
      instance.setAttribute("dom",      "")
 | 
			
		||||
      instance.setAttribute("show-texture-in-xr", "")  // only show aframe-html in xr 
 | 
			
		||||
      instance.setAttribute("grabbable","")
 | 
			
		||||
      instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
 | 
			
		||||
 | 
			
		||||
      const setupWindow = () => {
 | 
			
		||||
        const com = instance.components['helloworld-htmlform']
 | 
			
		||||
        instance.dom.style.display = 'none'
 | 
			
		||||
        new WinBox("Hello World",{ 
 | 
			
		||||
          width: 250,
 | 
			
		||||
          height: 340,
 | 
			
		||||
          x:"center",
 | 
			
		||||
          y:"center",
 | 
			
		||||
          id:  instance.uid, // important hint for html-mesh  
 | 
			
		||||
          root: document.querySelector("#overlay"),
 | 
			
		||||
          mount: instance.dom,
 | 
			
		||||
          onclose: () => { instance.dom.style.display = 'none'; return false; },
 | 
			
		||||
          oncreate: () => {
 | 
			
		||||
            instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
 | 
			
		||||
            instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
 | 
			
		||||
            instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        instance.dom.style.display = AFRAME.utils.XD() == '3D' ? 'none' : '' // show/hide
 | 
			
		||||
 | 
			
		||||
        // hint grabbable's obb-collider to track the window-object 
 | 
			
		||||
        instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
 | 
			
		||||
        instance.components['obb-collider'].update() 
 | 
			
		||||
 | 
			
		||||
        // data2event demo
 | 
			
		||||
        instance.setAttribute("data2event","")
 | 
			
		||||
        com.data.myvalue = 1
 | 
			
		||||
        com.data.foo     = `instance ${instance.uid}: `
 | 
			
		||||
        setInterval( () => com.data.myvalue++, 500 )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setTimeout( () => setupWindow(), 10 ) // give new components time to init
 | 
			
		||||
      this.el.setAttribute("dom", "")
 | 
			
		||||
      this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    ready: function( ){ 
 | 
			
		||||
      this.el.dom.style.display = 'none'
 | 
			
		||||
      console.log("this.el.dom has been added to DOM")
 | 
			
		||||
      this.data.myvalue = 1
 | 
			
		||||
    DOMready: function(){
 | 
			
		||||
      this.el.setAttribute("window", `title: Welcome; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; width:250; height: 360`)
 | 
			
		||||
 | 
			
		||||
      // data2event demo
 | 
			
		||||
      this.el.setAttribute("data2event","")
 | 
			
		||||
      this.data.myvalue = 1001
 | 
			
		||||
      this.data.foo     = `this.el ${this.el.uid}: `
 | 
			
		||||
      setInterval( () => this.data.myvalue++, 500 )
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    "window.oncreate": function(){
 | 
			
		||||
      this.el.setAttribute("html-as-texture-in-xr", `domid: .winbox#${this.el.uid}; faceuser: true`)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -34,69 +34,62 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){
 | 
			
		|||
 | 
			
		||||
  AFRAME.registerComponent('html-as-texture-in-xr', {
 | 
			
		||||
    schema: {
 | 
			
		||||
      domid: { type:"string"},
 | 
			
		||||
      faceuser: { type: "boolean", default: false}
 | 
			
		||||
      domid:       { type:"string"},
 | 
			
		||||
      doublesided: {type: "boolean", default: true},
 | 
			
		||||
      faceuser:    { type: "boolean", default: false}
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    dependencies:{
 | 
			
		||||
      html:        "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js",  // html to AFRAME
 | 
			
		||||
      //html:          "https://coderofsalvation.github.io/aframe-htmlmesh/build/aframe-html.js"
 | 
			
		||||
      //html:          "com/aframe-html.js"
 | 
			
		||||
      html:          "com/lib/aframe-html.js"
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    init: async function () { 
 | 
			
		||||
      let el = document.querySelector(this.data.domid)
 | 
			
		||||
      if( ! el ){
 | 
			
		||||
        return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.dom.id)
 | 
			
		||||
        return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.domid)
 | 
			
		||||
      }
 | 
			
		||||
      let s = await AFRAME.utils.require(this.dependencies)
 | 
			
		||||
      this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`)
 | 
			
		||||
 | 
			
		||||
      this.forwardClickToMesh();
 | 
			
		||||
 | 
			
		||||
      this.el.sceneEl.addEventListener('enter-vr', () => this.enableDoubleSided() )
 | 
			
		||||
 | 
			
		||||
      this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; `)
 | 
			
		||||
      this.el.setAttribute("visible",  AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
 | 
			
		||||
      if( this.data.faceuser ){
 | 
			
		||||
        this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    manifest: { // HTML5 manifest to identify app to xrsh
 | 
			
		||||
      "short_name": "show-texture-in-xr",
 | 
			
		||||
      "name": "2D/3D switcher",
 | 
			
		||||
      "icons": [],
 | 
			
		||||
      "id": "/?source=pwa",
 | 
			
		||||
      "start_url": "/?source=pwa",
 | 
			
		||||
      "background_color": "#3367D6",
 | 
			
		||||
      "display": "standalone",
 | 
			
		||||
      "scope": "/",
 | 
			
		||||
      "theme_color": "#3367D6",
 | 
			
		||||
      "category":"system",
 | 
			
		||||
      "shortcuts": [
 | 
			
		||||
        {
 | 
			
		||||
          "name": "What is the latest news?",
 | 
			
		||||
          "cli":{
 | 
			
		||||
            "usage":  "helloworld <type> [options]",
 | 
			
		||||
            "example": "helloworld news",
 | 
			
		||||
            "args":{
 | 
			
		||||
              "--latest": {type:"string"}
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          "short_name": "Today",
 | 
			
		||||
          "description": "View weather information for today",
 | 
			
		||||
          "url": "/today?source=pwa",
 | 
			
		||||
          "icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "description": "use ESC-key to toggle between 2D / 3D",
 | 
			
		||||
      "screenshots": [
 | 
			
		||||
        {
 | 
			
		||||
          "src": "/images/screenshot1.png",
 | 
			
		||||
          "type": "image/png",
 | 
			
		||||
          "sizes": "540x720",
 | 
			
		||||
          "form_factor": "narrow"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "help":`
 | 
			
		||||
  Helloworld application
 | 
			
		||||
    forwardClickToMesh: function(){
 | 
			
		||||
      // monkeypatch: forward click to mesh
 | 
			
		||||
      const handle = AFRAME.components['html'].Component.prototype.handle
 | 
			
		||||
      AFRAME.components['html'].Component.prototype.handle = function(type,evt){
 | 
			
		||||
 | 
			
		||||
      `
 | 
			
		||||
        if( !this.el.sceneEl.renderer.xr.isPresenting                        ) return // ignore events in desktop mode 
 | 
			
		||||
        if( this.el.sceneEl.renderer.xr.isPresenting && type.match(/^mouse/) ) return // ignore mouse-events in XR
 | 
			
		||||
 | 
			
		||||
        if( type == 'click' && evt.detail.length && evt.detail[0].uv ){
 | 
			
		||||
          const mesh = this.el.object3D.children[0] 
 | 
			
		||||
          const uv = evt.detail[0].uv;
 | 
			
		||||
          const _pointer = new THREE.Vector2();
 | 
			
		||||
          const _event = { type: '', data: _pointer };
 | 
			
		||||
          _event.type = type;
 | 
			
		||||
          _event.data.set( uv.x, 1 - uv.y );
 | 
			
		||||
          mesh.dispatchEvent( _event );
 | 
			
		||||
        }
 | 
			
		||||
        return handle.apply(this,[type,evt])
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    enableDoubleSided: function(){
 | 
			
		||||
      // enable doubleside
 | 
			
		||||
      this.el.object3D.traverse( (o) => {
 | 
			
		||||
        if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
 | 
			
		||||
          o.material.side = THREE.DoubleSide
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -140,7 +140,7 @@ if( typeof AFRAME != 'undefined '){
 | 
			
		|||
      css:     (me) => `
 | 
			
		||||
 | 
			
		||||
                        .isoterminal{
 | 
			
		||||
                          padding: ${me.com.data.padding}px;
 | 
			
		||||
                          padding: ${me.com.data.padding}px 0px 0px ${me.com.data.padding}px;
 | 
			
		||||
                          width:100%;
 | 
			
		||||
                          height:99%;
 | 
			
		||||
                          resize: both;
 | 
			
		||||
| 
						 | 
				
			
			@ -252,6 +252,8 @@ if( typeof AFRAME != 'undefined '){
 | 
			
		|||
                          background: transparent;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        .wb-control { margin-right:10px }
 | 
			
		||||
 | 
			
		||||
                        .XR .isoterminal{
 | 
			
		||||
                          background: #000;
 | 
			
		||||
                        }
 | 
			
		||||
| 
						 | 
				
			
			@ -336,7 +338,7 @@ if( typeof AFRAME != 'undefined '){
 | 
			
		|||
        this.term.emit('term_init', {instance, aEntity:this})
 | 
			
		||||
        //instance.winbox.resize(720,380)
 | 
			
		||||
        let size = `width: ${this.data.width}; height: ${this.data.height}`
 | 
			
		||||
        instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize`)
 | 
			
		||||
        instance.setAttribute("window", `title: xrsh; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize, no-close; `)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      instance.addEventListener('window.oncreate', (e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -372,7 +374,7 @@ if( typeof AFRAME != 'undefined '){
 | 
			
		|||
        const w = instance.winbox
 | 
			
		||||
        if(!w) return
 | 
			
		||||
        w.titleBak = w.titleBak || w.title
 | 
			
		||||
        w.setTitle( `${w.titleBak} [${msg}]` )
 | 
			
		||||
        w.setTitle( `${w.titleBak} ${msg ? "["+msg+"]" : ""}` )
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      instance.addEventListener('window.onclose', (e) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -383,18 +385,13 @@ if( typeof AFRAME != 'undefined '){
 | 
			
		|||
      instance.addEventListener('window.onresize', resize )
 | 
			
		||||
      instance.addEventListener('window.onmaximize', resize )
 | 
			
		||||
 | 
			
		||||
      const focus = (showdom) => (e) => {
 | 
			
		||||
      const focus = (e) => {
 | 
			
		||||
        this.el.emit('focus',e.detail)
 | 
			
		||||
        if( this.el.components.window && this.data.renderer == 'canvas'){
 | 
			
		||||
          this.el.components.window.show( showdom )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.el.addEventListener('obbcollisionstarted', focus(false) )
 | 
			
		||||
      this.el.sceneEl.addEventListener('enter-vr', focus(false) )
 | 
			
		||||
      this.el.sceneEl.addEventListener('enter-ar', focus(false) )
 | 
			
		||||
      this.el.sceneEl.addEventListener('exit-vr', focus(true) )
 | 
			
		||||
      this.el.sceneEl.addEventListener('exit-ar', focus(true) )
 | 
			
		||||
      this.el.addEventListener('obbcollisionstarted', focus )
 | 
			
		||||
      this.el.sceneEl.addEventListener('exit-vr', focus )
 | 
			
		||||
      this.el.sceneEl.addEventListener('exit-ar', focus )
 | 
			
		||||
 | 
			
		||||
      instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -444,7 +441,12 @@ if( typeof AFRAME != 'undefined '){
 | 
			
		|||
      myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
 | 
			
		||||
 | 
			
		||||
      launcher: async function(){
 | 
			
		||||
        this.initTerminal()
 | 
			
		||||
        if( !this.term.instance ){
 | 
			
		||||
          this.initTerminal()
 | 
			
		||||
        }else{
 | 
			
		||||
          // toggle visibility
 | 
			
		||||
          this.el.winbox[ this.el.winbox.min ? 'restore' : 'minimize' ]()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -266,13 +266,13 @@ ISOTerminal.prototype.startVM = function(opts){
 | 
			
		|||
  let msglib = this.getLoaderMsg()
 | 
			
		||||
  let msg = msglib.motd
 | 
			
		||||
 | 
			
		||||
  this.emit('status',msglib.loadmsg)
 | 
			
		||||
  this.emit('serial-output-string', msg)
 | 
			
		||||
  this.emit('bootMenu',{bootMenu: this.opts.bootMenu, bootMenuURL: this.opts.bootMenuURL })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ISOTerminal.prototype.bootISO = function(){
 | 
			
		||||
  let msglib = this.getLoaderMsg()
 | 
			
		||||
  this.emit('status',msglib.loadmsg)
 | 
			
		||||
  let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset
 | 
			
		||||
  this.emit('serial-output-string', msg)
 | 
			
		||||
  this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,8 @@ ISOTerminal.addEventListener('ready', function(e){
 | 
			
		|||
 | 
			
		||||
ISOTerminal.prototype.bootMenu = function(e){
 | 
			
		||||
  this.boot.menu.selected = false // reset
 | 
			
		||||
  const autobootURL = e.detail.bootMenuURL && document.location.hash.length > 1
 | 
			
		||||
  const autoboot    = e.detail.bootMenu || autobootURL
 | 
			
		||||
  const autobootURL = e && e.detail.bootMenuURL && document.location.hash.length > 1
 | 
			
		||||
  const autoboot    = e && e.detail.bootMenu || autobootURL
 | 
			
		||||
  if( !autoboot ){
 | 
			
		||||
 | 
			
		||||
    let msg = '\n\r'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -96,10 +96,10 @@ ISOTerminal.addEventListener('init', function(){
 | 
			
		|||
      },
 | 
			
		||||
      keyHandler: function(ch){
 | 
			
		||||
        let erase = false
 | 
			
		||||
        if( ch == '\x7F' ){
 | 
			
		||||
          ch = "\b \b" // why does write() not just support \x7F ?
 | 
			
		||||
          erase = true
 | 
			
		||||
        }
 | 
			
		||||
//        if( ch == '\x7F' ){
 | 
			
		||||
//          ch = "\b \b" // why does write() not just support \x7F ?
 | 
			
		||||
//          erase = true
 | 
			
		||||
//        }
 | 
			
		||||
        this.send(ch)
 | 
			
		||||
        const reset = () => {
 | 
			
		||||
          this.console = ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,7 +92,11 @@ ISOTerminal.prototype.TermInit = function(){
 | 
			
		|||
      }
 | 
			
		||||
      if( !erase ) this.lastChar = ch
 | 
			
		||||
    })
 | 
			
		||||
    aEntity.el.addEventListener('focus', () => el.querySelector("textarea").focus() )
 | 
			
		||||
    aEntity.el.addEventListener('focus', () => {
 | 
			
		||||
      let textarea = el.querySelector("textarea")
 | 
			
		||||
      textarea.focus()
 | 
			
		||||
      if( document.activeElement != textarea ) textarea.focus() 
 | 
			
		||||
    })
 | 
			
		||||
    aEntity.el.addEventListener('serial-output-string', (e) => {
 | 
			
		||||
      let msg = e.detail
 | 
			
		||||
      this.term.write(msg)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										313
									
								
								com/launcher.js
									
										
									
									
									
								
							
							
						
						
									
										313
									
								
								com/launcher.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,20 +1,52 @@
 | 
			
		|||
/*
 | 
			
		||||
 * ## launcher
 | 
			
		||||
/** 
 | 
			
		||||
 * ## [launcher](com/launcher.js)
 | 
			
		||||
 *
 | 
			
		||||
 * displays app (icons) for enduser to launch
 | 
			
		||||
 * displays app (icons) in 2D and 3D handmenu (enduser can launch desktop-like 'apps')
 | 
			
		||||
 *
 | 
			
		||||
 * ```javascript
 | 
			
		||||
 *  <a-entity app="app/launcher.js"/>
 | 
			
		||||
 * ```html
 | 
			
		||||
 * <a-entity launcher>
 | 
			
		||||
 *   <a-entity launch="component: helloworld; foo: bar"><a-entity>
 | 
			
		||||
 * </a-entity>
 | 
			
		||||
 *  
 | 
			
		||||
 * ```
 | 
			
		||||
 *
 | 
			
		||||
 * | property     | type               | example                                                                                |
 | 
			
		||||
 * |--------------|--------------------|----------------------------------------------------------------------------------------|
 | 
			
		||||
 * | `registries` | `array` of strings | <a-entity app="app/launcher.js; registers: https://foo.com/index.json, ./index.json"/> |
 | 
			
		||||
 * | `attach`     | `selector`         | hand or object to attach menu to                                                       |
 | 
			
		||||
 * | `registries` | `array` of strings | `<a-entity launcher="registers: https://foo.com/index.json, ./index.json"/>`           |
 | 
			
		||||
 *
 | 
			
		||||
 * | event        | target | info                                                                                               |
 | 
			
		||||
 * |--------------|-------------------------------------------------------------------------------------------------------------|
 | 
			
		||||
 * | `launcher`   | an app | when pressing an app icon, `launcher` event will be send to the respective app                     |
 | 
			
		||||
 */
 | 
			
		||||
 *
 | 
			
		||||
 * There a multiple ways of letting the launcher know that an app can be launched:
 | 
			
		||||
 *
 | 
			
		||||
 * 1. any AFRAME component with an `launcher`-event + manifest is automatically added:
 | 
			
		||||
*  
 | 
			
		||||
*  ```javascript
 | 
			
		||||
*  AFRAME.registerComponent('foo',{
 | 
			
		||||
*    events:{
 | 
			
		||||
*      launcher: function(){ ...launch something... }
 | 
			
		||||
*    },
 | 
			
		||||
*    manifest:{ // HTML5 manifesto JSON object
 | 
			
		||||
*      // https://www.w3.org/TR/appmanifest/ 
 | 
			
		||||
*    }
 | 
			
		||||
*  }
 | 
			
		||||
*  ```
 | 
			
		||||
*
 | 
			
		||||
* 2. dynamically in javascript   
 | 
			
		||||
*
 | 
			
		||||
* ```javascript
 | 
			
		||||
* window.launcher.register({
 | 
			
		||||
*   name:"foo",
 | 
			
		||||
*   icon: "https://.../optional_icon.png" 
 | 
			
		||||
*   description: "lorem ipsum",
 | 
			
		||||
*   cb: () => alert("foo")
 | 
			
		||||
* })
 | 
			
		||||
* //window.launcher.unregister('foo')
 | 
			
		||||
* ```
 | 
			
		||||
* 
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
AFRAME.registerComponent('launch', { // use this component to auto-launch component
 | 
			
		||||
  init: function(){ 
 | 
			
		||||
| 
						 | 
				
			
			@ -26,12 +58,6 @@ AFRAME.registerComponent('launch', { // use this component to auto-launch compon
 | 
			
		|||
 | 
			
		||||
AFRAME.registerComponent('launcher', {
 | 
			
		||||
  schema: {
 | 
			
		||||
    attach: { type:"selector"},
 | 
			
		||||
    padding: { type:"number","default":0.15},
 | 
			
		||||
    fingerTip: {type:"selector"},
 | 
			
		||||
    fingerDistance: {type:"number", "default":0.25},
 | 
			
		||||
    rescale: {type:"number","default":0.4},
 | 
			
		||||
    open: { type:"boolean", "default":true},
 | 
			
		||||
    colors: { type:"array", "default": [
 | 
			
		||||
      '#4C73FE',
 | 
			
		||||
      '#554CFE',
 | 
			
		||||
| 
						 | 
				
			
			@ -42,52 +68,32 @@ AFRAME.registerComponent('launcher', {
 | 
			
		|||
      '#333333',
 | 
			
		||||
    ]},
 | 
			
		||||
    paused: { type:"boolean","default":false},
 | 
			
		||||
    cols:   { type:"number", "default": 5 }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dependencies:{
 | 
			
		||||
    dom:         "com/dom.js"
 | 
			
		||||
  requires:{
 | 
			
		||||
    dom:         "com/dom.js",
 | 
			
		||||
    htmlinxr:    "com/html-as-texture-in-xr.js",
 | 
			
		||||
    data2events: "com/data2event.js"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  init: async function () {
 | 
			
		||||
    await AFRAME.utils.require(this.dependencies)
 | 
			
		||||
    this.el.object3D.visible = false;
 | 
			
		||||
    await AFRAME.utils.require(this.requires)
 | 
			
		||||
    this.worldPosition = new THREE.Vector3()
 | 
			
		||||
 | 
			
		||||
    await AFRAME.utils.require({
 | 
			
		||||
      html:        "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js",  // html to AFRAME
 | 
			
		||||
      dom:         "./com/dom.js",
 | 
			
		||||
      data2events: "./com/data2event.js"
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.el.setAttribute("dom","")
 | 
			
		||||
    this.el.setAttribute("noxd","ignore") // hint to XD.js that we manage ourselve concerning 2D/3D switching
 | 
			
		||||
    this.el.setAttribute("pressable","")
 | 
			
		||||
    this.el.sceneEl.addEventListener('enter-vr', () => this.centerMenu() )
 | 
			
		||||
    this.el.sceneEl.addEventListener('enter-ar', () => this.centerMenu() )
 | 
			
		||||
    this.render()
 | 
			
		||||
 | 
			
		||||
    if( this.data.attach ){
 | 
			
		||||
      this.el.object3D.visible = false
 | 
			
		||||
      if( this.isHand(this.data.attach) ){
 | 
			
		||||
        this.data.attach.addEventListener('model-loaded', () => {
 | 
			
		||||
          this.ready = true
 | 
			
		||||
          this.attachMenu() 
 | 
			
		||||
        })
 | 
			
		||||
        // add button
 | 
			
		||||
        this.menubutton = this.createMenuButton()
 | 
			
		||||
        this.menubutton.object3D.visible = false
 | 
			
		||||
        this.data.attach.appendChild( this.menubutton )
 | 
			
		||||
      }else this.data.attach.appendChild(this.el)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  isHand: (el) => {
 | 
			
		||||
    return el.getAttributeNames().filter( (n) => n.match(/^hand-tracking/) ? n : null ).length ? true : false
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dom: {
 | 
			
		||||
    scale:   3,
 | 
			
		||||
    scale:   0.8,
 | 
			
		||||
    events:  ['click'],
 | 
			
		||||
    html:    (me) => `<div class="iconmenu">loading components..</div>`,
 | 
			
		||||
    css:     (me) => `.iconmenu {
 | 
			
		||||
    css:     (me) => `
 | 
			
		||||
              .iconmenu {
 | 
			
		||||
                z-index: 1000;
 | 
			
		||||
                display: flex;
 | 
			
		||||
                flex-direction: row;
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +101,6 @@ AFRAME.registerComponent('launcher', {
 | 
			
		|||
                height: 50px;
 | 
			
		||||
                overflow:hidden;
 | 
			
		||||
                position: fixed;
 | 
			
		||||
                right: 162px;
 | 
			
		||||
                bottom: 10px;
 | 
			
		||||
                left:20px;
 | 
			
		||||
                background: transparent;
 | 
			
		||||
| 
						 | 
				
			
			@ -116,6 +121,8 @@ AFRAME.registerComponent('launcher', {
 | 
			
		|||
                border-top: 2px solid #BBB;
 | 
			
		||||
                border-bottom: 2px solid #BBB;
 | 
			
		||||
                font-size:18px;
 | 
			
		||||
                color: #777;
 | 
			
		||||
                line-height: 7px;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              .iconmenu > button:first-child {
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +139,10 @@ AFRAME.registerComponent('launcher', {
 | 
			
		|||
                padding-top:13px;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              .iconmenu > button:only-child{
 | 
			
		||||
                border-radius:5px 5px 5px 5px;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              .iconmenu > button > img {
 | 
			
		||||
                transform: translate(0px,-14px);
 | 
			
		||||
                opacity:0.5;
 | 
			
		||||
| 
						 | 
				
			
			@ -146,15 +157,11 @@ AFRAME.registerComponent('launcher', {
 | 
			
		|||
  },
 | 
			
		||||
 | 
			
		||||
  events:{
 | 
			
		||||
    open: function(){
 | 
			
		||||
      this.preventAccidentalButtonPresses()
 | 
			
		||||
      if( this.data.open ){
 | 
			
		||||
        this.el.setAttribute("animation",`dur: 200; property: scale; from: 0 0 1; to: ${this.data.rescale} ${this.data.rescale} ${this.data.rescale}`)
 | 
			
		||||
        this.menubutton.object3D.visible = false
 | 
			
		||||
      }else{
 | 
			
		||||
        this.el.setAttribute("animation",`dur: 200; property: scale; from: ${this.data.rescale} ${this.data.rescale} ${this.data.rescale}; to: 0 0 1`)
 | 
			
		||||
        this.menubutton.object3D.visible = true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    click: function(e){ },
 | 
			
		||||
 | 
			
		||||
    DOMready: function(){
 | 
			
		||||
      this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}; faceuser: false`)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -163,136 +170,70 @@ AFRAME.registerComponent('launcher', {
 | 
			
		|||
    setTimeout( () => this.data.paused = false, 500 ) // prevent menubutton press collide with animated buttons
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  createMenuButton: function(colo){
 | 
			
		||||
    let aentity = document.createElement('a-entity')
 | 
			
		||||
    aentity.setAttribute("mixin","menubutton")
 | 
			
		||||
    aentity.addEventListener('obbcollisionstarted', this.onpress )
 | 
			
		||||
    aentity.addEventListener('obbcollisionended', this.onreleased )
 | 
			
		||||
    return aentity
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  render: async function(els){
 | 
			
		||||
    if( !this.el.dom ) return // too early (dom.js component not ready)
 | 
			
		||||
 | 
			
		||||
    let requires = [] 
 | 
			
		||||
    let i        = 0 
 | 
			
		||||
    let j        = 0
 | 
			
		||||
    let colors   = this.data.colors 
 | 
			
		||||
    const add2D = (launchCom,el,manifest) => {
 | 
			
		||||
    const add2D = (launchCom,manifest) => {
 | 
			
		||||
      let btn    = document.createElement('button')
 | 
			
		||||
      btn.innerHTML = `${ manifest?.icons?.length > 0  
 | 
			
		||||
                             ? `<img src='${manifest.icons[0].src}' title='${manifest.name}: ${manifest.description}'/>` 
 | 
			
		||||
                             : `${manifest.short_name}`
 | 
			
		||||
                      }`
 | 
			
		||||
      btn.addEventListener('click', () => el.emit('launcher',{}) )
 | 
			
		||||
      this.el.dom.appendChild(btn)
 | 
			
		||||
    }
 | 
			
		||||
      let iconDefault = "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMjAuMTcwMiAzTDIwLjE2NjMgMy4wMDQ1M0MyMS43NDU4IDMuMDkwODQgMjMgNC4zOTg5NiAyMyA2VjE4QzIzIDE5LjY1NjkgMjEuNjU2OSAyMSAyMCAyMUg0QzIuMzQzMTUgMjEgMSAxOS42NTY5IDEgMThWNkMxIDQuMzQzMTUgMi4zNDMxNSAzIDQgM0gyMC4xNzAyWk0xMC40NzY0IDVIMTYuNDc2NEwxMy4wODkgOUg3LjA4ODk5TDEwLjQ3NjQgNVpNNS4wODg5OSA5TDguNDc2NDQgNUg0QzMuNDQ3NzIgNSAzIDUuNDQ3NzIgMyA2VjlINS4wODg5OVpNMyAxMVYxOEMzIDE4LjU1MjMgMy40NDc3MiAxOSA0IDE5SDIwQzIwLjU1MjMgMTkgMjEgMTguNTUyMyAyMSAxOFYxMUgzWk0yMSA5VjZDMjEgNS40NDc3MSAyMC41NTIzIDUgMjAgNUgxOC40NzY0TDE1LjA4OSA5SDIxWiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgo8L3N2Zz4="
 | 
			
		||||
      let html    = manifest?.icons?.length > 0 || !manifest.name ? `<img src='${manifest.icons[0].src || iconDefault}' title='${manifest.name}: ${manifest.description}'/>` : ""
 | 
			
		||||
      if( manifest.name && !html ) html = `${manifest.short_name || manifest.name}`
 | 
			
		||||
      btn.innerHTML = html 
 | 
			
		||||
 | 
			
		||||
    const add3D = (launchCom,el,manifest) => {
 | 
			
		||||
      let aentity = document.createElement('a-entity')
 | 
			
		||||
      let atext   = document.createElement('a-entity')
 | 
			
		||||
      let padding = this.data.padding
 | 
			
		||||
      if( (i % this.data.cols) == 0 ) j++
 | 
			
		||||
      aentity.setAttribute("mixin","menuitem")
 | 
			
		||||
      aentity.setAttribute("position",`${padding+(i++ % this.data.cols) * padding} ${j*padding} 0`)
 | 
			
		||||
      if( !aentity.getAttribute("material")){
 | 
			
		||||
        aentity.setAttribute('material',`side: double; color: ${colors[ i % colors.length]}`)
 | 
			
		||||
      }
 | 
			
		||||
      aentity.addEventListener('obbcollisionstarted', this.onpress )
 | 
			
		||||
      aentity.addEventListener('obbcollisionended', this.onreleased )
 | 
			
		||||
      atext.setAttribute("text",`value: ${manifest.short_name}; align: baseline; anchor: align; align:center; wrapCount:7`)
 | 
			
		||||
      atext.setAttribute("scale","0.1 0.1 0.1")
 | 
			
		||||
      aentity.appendChild(atext)
 | 
			
		||||
      this.el.appendChild(aentity)
 | 
			
		||||
      aentity.launchCom = launchCom
 | 
			
		||||
      return aentity
 | 
			
		||||
      btn.addEventListener('click', (e) => {
 | 
			
		||||
        launchCom.launcher()
 | 
			
		||||
        e.stopPropagation()
 | 
			
		||||
        // visual feedback to user
 | 
			
		||||
        btn.style.filter = "brightness(0.5)"
 | 
			
		||||
        this.el.components.html.rerender()
 | 
			
		||||
        setTimeout(  () => {
 | 
			
		||||
          btn.style.filter = "brightness(1)"
 | 
			
		||||
          this.el.components.html.rerender()
 | 
			
		||||
        }, 500 )
 | 
			
		||||
        return false
 | 
			
		||||
      })
 | 
			
		||||
      this.el.dom.appendChild(btn)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // finally render them!
 | 
			
		||||
    this.el.dom.innerHTML = '' // clear
 | 
			
		||||
    els = els || this.system.components
 | 
			
		||||
    els = els || this.system.launchables
 | 
			
		||||
    els.map( (c) => {
 | 
			
		||||
      const launchComponentKey = c.getAttributeNames().shift()
 | 
			
		||||
      const launchCom          = c.components[ launchComponentKey ]
 | 
			
		||||
      if( !launchCom ) return console.warn(`could not find component '${launchComponentKey}' (forgot to include script-tag?)`)
 | 
			
		||||
      const manifest           = launchCom.manifest
 | 
			
		||||
      const manifest           = c.manifest
 | 
			
		||||
      if( manifest ){
 | 
			
		||||
        add2D(launchCom,c,manifest)
 | 
			
		||||
        add3D(launchCom,c,manifest)
 | 
			
		||||
        add2D(c,manifest)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.centerMenu();
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onpress: function(e){
 | 
			
		||||
    const launcher = document.querySelector('[launcher]').components.launcher
 | 
			
		||||
    if( launcher.data.paused                           ) return // prevent accidental pressed due to animation
 | 
			
		||||
    if( e.detail.withEl.computedMixinStr == 'menuitem' ) return // dont react to menuitems touching eachother
 | 
			
		||||
 | 
			
		||||
    // if user press menu button toggle menu
 | 
			
		||||
    if( launcher && !launcher.data.open && e.srcElement.computedMixinStr == 'menubutton' ){
 | 
			
		||||
      return (launcher.data.open = true)
 | 
			
		||||
    }
 | 
			
		||||
    if( launcher && !launcher.data.open ) return // dont process menuitems when menu is closed
 | 
			
		||||
    let el = e.srcElement
 | 
			
		||||
    if(!el) return
 | 
			
		||||
    el.object3D.traverse( (o) => {
 | 
			
		||||
      if( o.material && o.material.color ){
 | 
			
		||||
        if( !o.material.colorOriginal ) o.material.colorOriginal = o.material.color.clone() 
 | 
			
		||||
        o.material.color.r *= 0.3
 | 
			
		||||
        o.material.color.g *= 0.3
 | 
			
		||||
        o.material.color.b *= 0.3
 | 
			
		||||
  centerMenu: function(){
 | 
			
		||||
    // center along x-axis
 | 
			
		||||
    this.el.object3D.traverse( (o) => {
 | 
			
		||||
      if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
 | 
			
		||||
        this.setOriginToMiddle(o, this.el.object3D, {x:true})
 | 
			
		||||
        o.position.z = 0.012 // position a bit before the grab-line
 | 
			
		||||
        o.position.y = -0.01 // position a bit before the grab-line
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    if( el.launchCom ){
 | 
			
		||||
      console.log("launcher.js: launching "+el.launchCom.el.getAttributeNames().shift())
 | 
			
		||||
      launcher.preventAccidentalButtonPresses()
 | 
			
		||||
      el.launchCom.el.emit('launcher') // launch component!
 | 
			
		||||
      this.data.open = false // close to prevent infinite loop of clicks when leaving immersive mode
 | 
			
		||||
    }
 | 
			
		||||
    this.el.object3D.visible = true // ensure visibility
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onreleased: function(e){
 | 
			
		||||
    if( e.detail.withEl.computedMixinStr == 'menuitem' ) return // dont react to menuitems touching eachother
 | 
			
		||||
    let el = e.srcElement
 | 
			
		||||
    el.object3D.traverse( (o) => {
 | 
			
		||||
      if( o.material && o.material.color ){
 | 
			
		||||
        if( o.material.colorOriginal ) o.material.color = o.material.colorOriginal.clone() 
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  setOriginToMiddle: function(fromObject, toObject, axis) {
 | 
			
		||||
    var boxFrom = new THREE.Box3().setFromObject(fromObject);
 | 
			
		||||
    var boxTo   = new THREE.Box3().setFromObject(toObject);
 | 
			
		||||
    var center = new THREE.Vector3();
 | 
			
		||||
    if( !toObject.positionOriginal ) toObject.positionOriginal = toObject.position.clone()
 | 
			
		||||
    center.x = axis.x ? - (boxFrom.max.x/2) : 0
 | 
			
		||||
    center.y = axis.y ? - (boxFrom.max.y/2) : 0
 | 
			
		||||
    center.z = axis.z ? - (boxFrom.max.z/2) : 0
 | 
			
		||||
    toObject.position.copy( toObject.positionOriginal )
 | 
			
		||||
    toObject.position.sub(center);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  attachMenu: function(){
 | 
			
		||||
    if( this.el.parentNode != this.data.attach ){
 | 
			
		||||
      this.el.object3D.visible = true
 | 
			
		||||
      let armature = this.data.attach.object3D.getObjectByName('Armature') 
 | 
			
		||||
      if( !armature ) return console.warn('cannot find armature')
 | 
			
		||||
      this.data.attach.object3D.children[0].add(this.el.object3D)
 | 
			
		||||
      this.el.object3D.scale.x = this.data.rescale
 | 
			
		||||
      this.el.object3D.scale.y = this.data.rescale
 | 
			
		||||
      this.el.object3D.scale.z = this.data.rescale
 | 
			
		||||
 | 
			
		||||
      // add obb-collider to index finger-tip
 | 
			
		||||
      let aentity = document.createElement('a-entity')
 | 
			
		||||
      trackedObject3DVariable = 'parentNode.components.hand-tracking-controls.bones.9';
 | 
			
		||||
      this.data.fingerTip.appendChild(aentity)
 | 
			
		||||
      aentity.setAttribute('obb-collider', {trackedObject3D: trackedObject3DVariable, size: 0.015});
 | 
			
		||||
 | 
			
		||||
      if( this.isHand(this.data.attach) ){
 | 
			
		||||
        // shortly show and hide menu into palm (hint user)
 | 
			
		||||
        setTimeout( () => { this.data.open = false }, 1500 )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  tick: function(){
 | 
			
		||||
    if( this.ready && this.data.open ){
 | 
			
		||||
      let indexTipPosition = document.querySelector('#right-hand[hand-tracking-controls]').components['hand-tracking-controls'].indexTipPosition
 | 
			
		||||
      this.el.object3D.getWorldPosition(this.worldPosition)
 | 
			
		||||
      const lookingAtPalm = this.data.attach.components['hand-tracking-controls'].wristObject3D.rotation.z > 2.0 
 | 
			
		||||
      if( !lookingAtPalm ){ this.data.open = false }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  manifest: { // HTML5 manifest to identify app to xrsh
 | 
			
		||||
    "short_name": "launcher",
 | 
			
		||||
| 
						 | 
				
			
			@ -352,35 +293,73 @@ in above's case "\nHelloworld application\n" will qualify as header.
 | 
			
		|||
AFRAME.registerSystem('launcher',{
 | 
			
		||||
 | 
			
		||||
  init: function(){
 | 
			
		||||
    this.components = []
 | 
			
		||||
    this.launchables = []
 | 
			
		||||
    this.dom         = []
 | 
			
		||||
    this.registered  = []
 | 
			
		||||
    // observe HTML changes in <a-scene>
 | 
			
		||||
    observer = new MutationObserver( (a,b) => this.getLaunchables(a,b) )
 | 
			
		||||
    observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
 | 
			
		||||
 | 
			
		||||
    window.launcher = this
 | 
			
		||||
    this.getLaunchables()
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  register: function(launchable){
 | 
			
		||||
    try{
 | 
			
		||||
      let {name, description, cb} = launchable
 | 
			
		||||
      this.registered.push({ 
 | 
			
		||||
        manifest: {name, description, icons: launchable.icon ? [{src:launchable.icon}] : [] },
 | 
			
		||||
        launcher: cb 
 | 
			
		||||
      })
 | 
			
		||||
    }catch(e){
 | 
			
		||||
      console.error('AFRAME.systems.launcher.register({ name, description, icon, cb }) got invalid obj')
 | 
			
		||||
      console.error(e)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.getLaunchables()
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  unregister: function(launchableName){
 | 
			
		||||
    this.registered = this.registered.filter( (l) => l.name != launchableName )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  getLaunchables: function(mutationsList,observer){
 | 
			
		||||
    let searchEvent = 'launcher'
 | 
			
		||||
    let els         = [...this.sceneEl.getElementsByTagName("*")]
 | 
			
		||||
    let seen        = {}
 | 
			
		||||
    this.launchables = [
 | 
			
		||||
      /* 
 | 
			
		||||
       * {
 | 
			
		||||
       *   manifest: {...}
 | 
			
		||||
       *   launcher: () => ....
 | 
			
		||||
       * }
 | 
			
		||||
       */ 
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    this.components = els.filter( (el) => {
 | 
			
		||||
    // collect manually registered launchables
 | 
			
		||||
    this.registered.map( (launchable) => this.launchables.push(launchable) )
 | 
			
		||||
 | 
			
		||||
    // collect launchables in aframe dom elements
 | 
			
		||||
    this.dom = els.filter( (el) => {
 | 
			
		||||
      let hasEvent = false
 | 
			
		||||
      if( el.components ){
 | 
			
		||||
        for( let i in el.components ){
 | 
			
		||||
          if( el.components[i].events && el.components[i].events[searchEvent] && !seen[i] ){
 | 
			
		||||
            hasEvent = seen[i] = true
 | 
			
		||||
            let com = hasEvent = seen[i] = el.components[i]
 | 
			
		||||
            com.launcher = () => com.el.emit('launcher',null,false) // important: no bubble
 | 
			
		||||
            this.launchables.push(com)
 | 
			
		||||
          }
 | 
			
		||||
        } 
 | 
			
		||||
      }
 | 
			
		||||
      return hasEvent ? el : null 
 | 
			
		||||
    })
 | 
			
		||||
    this.updateLauncher()
 | 
			
		||||
    return seen
 | 
			
		||||
    return this.launchables
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  updateLauncher: function(){
 | 
			
		||||
    let launcher = document.querySelector('[launcher]')
 | 
			
		||||
    if( launcher ) launcher.components.launcher.render()
 | 
			
		||||
    if( launcher && launcher.components.launcher) launcher.components.launcher.render()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										679
									
								
								com/lib/aframe-html.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										679
									
								
								com/lib/aframe-html.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,679 @@
 | 
			
		|||
(function (three) {
 | 
			
		||||
	'use strict';
 | 
			
		||||
 | 
			
		||||
	// This is a copy of https://github.com/mrdoob/three.js/blob/0403020848c26a9605eb91c99a949111ad4a532e/examples/jsm/interactive/HTMLMesh.js
 | 
			
		||||
 | 
			
		||||
	class HTMLMesh extends three.Mesh {
 | 
			
		||||
 | 
			
		||||
		constructor( dom ) {
 | 
			
		||||
 | 
			
		||||
			const texture = new HTMLTexture( dom );
 | 
			
		||||
 | 
			
		||||
			const geometry = new three.PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 );
 | 
			
		||||
			const material = new three.MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } );
 | 
			
		||||
 | 
			
		||||
			super( geometry, material );
 | 
			
		||||
 | 
			
		||||
			function onEvent( event ) {
 | 
			
		||||
 | 
			
		||||
				material.map.dispatchDOMEvent( event );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.addEventListener( 'mousedown', onEvent );
 | 
			
		||||
			this.addEventListener( 'mousemove', onEvent );
 | 
			
		||||
			this.addEventListener( 'mouseup', onEvent );
 | 
			
		||||
			this.addEventListener( 'click', onEvent );
 | 
			
		||||
 | 
			
		||||
			this.dispose = function () {
 | 
			
		||||
 | 
			
		||||
				geometry.dispose();
 | 
			
		||||
				material.dispose();
 | 
			
		||||
 | 
			
		||||
				material.map.dispose();
 | 
			
		||||
 | 
			
		||||
				canvases.delete( dom );
 | 
			
		||||
 | 
			
		||||
				this.removeEventListener( 'mousedown', onEvent );
 | 
			
		||||
				this.removeEventListener( 'mousemove', onEvent );
 | 
			
		||||
				this.removeEventListener( 'mouseup', onEvent );
 | 
			
		||||
				this.removeEventListener( 'click', onEvent );
 | 
			
		||||
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	class HTMLTexture extends three.CanvasTexture {
 | 
			
		||||
 | 
			
		||||
		constructor( dom ) {
 | 
			
		||||
 | 
			
		||||
			super( html2canvas( dom ) );
 | 
			
		||||
 | 
			
		||||
			this.dom = dom;
 | 
			
		||||
 | 
			
		||||
			this.anisotropy = 16;
 | 
			
		||||
			this.encoding = three.sRGBEncoding;
 | 
			
		||||
			this.minFilter = three.LinearFilter;
 | 
			
		||||
			this.magFilter = three.LinearFilter;
 | 
			
		||||
 | 
			
		||||
			// Create an observer on the DOM, and run html2canvas update in the next loop
 | 
			
		||||
			const observer = new MutationObserver( () => {
 | 
			
		||||
 | 
			
		||||
				if ( ! this.scheduleUpdate ) {
 | 
			
		||||
 | 
			
		||||
					// ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer
 | 
			
		||||
					this.scheduleUpdate = setTimeout( () => this.update(), 16 );
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			} );
 | 
			
		||||
 | 
			
		||||
			const config = { attributes: true, childList: true, subtree: true, characterData: true };
 | 
			
		||||
			observer.observe( dom, config );
 | 
			
		||||
 | 
			
		||||
			this.observer = observer;
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dispatchDOMEvent( event ) {
 | 
			
		||||
 | 
			
		||||
			if ( event.data ) {
 | 
			
		||||
 | 
			
		||||
				htmlevent( this.dom, event.type, event.data.x, event.data.y );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		update() {
 | 
			
		||||
 | 
			
		||||
			this.image = html2canvas( this.dom );
 | 
			
		||||
			this.needsUpdate = true;
 | 
			
		||||
 | 
			
		||||
			this.scheduleUpdate = null;
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dispose() {
 | 
			
		||||
 | 
			
		||||
			if ( this.observer ) {
 | 
			
		||||
 | 
			
		||||
				this.observer.disconnect();
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.scheduleUpdate = clearTimeout( this.scheduleUpdate );
 | 
			
		||||
 | 
			
		||||
			super.dispose();
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	//
 | 
			
		||||
 | 
			
		||||
	const canvases = new WeakMap();
 | 
			
		||||
 | 
			
		||||
	function html2canvas( element ) {
 | 
			
		||||
 | 
			
		||||
		const range = document.createRange();
 | 
			
		||||
		const color = new three.Color();
 | 
			
		||||
 | 
			
		||||
		function Clipper( context ) {
 | 
			
		||||
 | 
			
		||||
			const clips = [];
 | 
			
		||||
			let isClipping = false;
 | 
			
		||||
 | 
			
		||||
			function doClip() {
 | 
			
		||||
 | 
			
		||||
				if ( isClipping ) {
 | 
			
		||||
 | 
			
		||||
					isClipping = false;
 | 
			
		||||
					context.restore();
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if ( clips.length === 0 ) return;
 | 
			
		||||
 | 
			
		||||
				let minX = - Infinity, minY = - Infinity;
 | 
			
		||||
				let maxX = Infinity, maxY = Infinity;
 | 
			
		||||
 | 
			
		||||
				for ( let i = 0; i < clips.length; i ++ ) {
 | 
			
		||||
 | 
			
		||||
					const clip = clips[ i ];
 | 
			
		||||
 | 
			
		||||
					minX = Math.max( minX, clip.x );
 | 
			
		||||
					minY = Math.max( minY, clip.y );
 | 
			
		||||
					maxX = Math.min( maxX, clip.x + clip.width );
 | 
			
		||||
					maxY = Math.min( maxY, clip.y + clip.height );
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				context.save();
 | 
			
		||||
				context.beginPath();
 | 
			
		||||
				context.rect( minX, minY, maxX - minX, maxY - minY );
 | 
			
		||||
				context.clip();
 | 
			
		||||
 | 
			
		||||
				isClipping = true;
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return {
 | 
			
		||||
 | 
			
		||||
				add: function ( clip ) {
 | 
			
		||||
 | 
			
		||||
					clips.push( clip );
 | 
			
		||||
					doClip();
 | 
			
		||||
 | 
			
		||||
				},
 | 
			
		||||
 | 
			
		||||
				remove: function () {
 | 
			
		||||
 | 
			
		||||
					clips.pop();
 | 
			
		||||
					doClip();
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function drawText( style, x, y, string ) {
 | 
			
		||||
 | 
			
		||||
			if ( string !== '' ) {
 | 
			
		||||
 | 
			
		||||
				if ( style.textTransform === 'uppercase' ) {
 | 
			
		||||
 | 
			
		||||
					string = string.toUpperCase();
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
 | 
			
		||||
				context.textBaseline = 'top';
 | 
			
		||||
				context.fillStyle = style.color;
 | 
			
		||||
				context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function buildRectPath( x, y, w, h, r ) {
 | 
			
		||||
 | 
			
		||||
			if ( w < 2 * r ) r = w / 2;
 | 
			
		||||
			if ( h < 2 * r ) r = h / 2;
 | 
			
		||||
 | 
			
		||||
			context.beginPath();
 | 
			
		||||
			context.moveTo( x + r, y );
 | 
			
		||||
			context.arcTo( x + w, y, x + w, y + h, r );
 | 
			
		||||
			context.arcTo( x + w, y + h, x, y + h, r );
 | 
			
		||||
			context.arcTo( x, y + h, x, y, r );
 | 
			
		||||
			context.arcTo( x, y, x + w, y, r );
 | 
			
		||||
			context.closePath();
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function drawBorder( style, which, x, y, width, height ) {
 | 
			
		||||
 | 
			
		||||
			const borderWidth = style[ which + 'Width' ];
 | 
			
		||||
			const borderStyle = style[ which + 'Style' ];
 | 
			
		||||
			const borderColor = style[ which + 'Color' ];
 | 
			
		||||
 | 
			
		||||
			if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {
 | 
			
		||||
 | 
			
		||||
				context.strokeStyle = borderColor;
 | 
			
		||||
				context.lineWidth = parseFloat( borderWidth );
 | 
			
		||||
				context.beginPath();
 | 
			
		||||
				context.moveTo( x, y );
 | 
			
		||||
				context.lineTo( x + width, y + height );
 | 
			
		||||
				context.stroke();
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function drawElement( element, style ) {
 | 
			
		||||
 | 
			
		||||
			let x = 0, y = 0, width = 0, height = 0;
 | 
			
		||||
 | 
			
		||||
			if ( element.nodeType === Node.TEXT_NODE ) {
 | 
			
		||||
 | 
			
		||||
				// text
 | 
			
		||||
 | 
			
		||||
				range.selectNode( element );
 | 
			
		||||
 | 
			
		||||
				const rect = range.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
				x = rect.left - offset.left - 0.5;
 | 
			
		||||
				y = rect.top - offset.top - 0.5;
 | 
			
		||||
				width = rect.width;
 | 
			
		||||
				height = rect.height;
 | 
			
		||||
 | 
			
		||||
				drawText( style, x, y, element.nodeValue.trim() );
 | 
			
		||||
 | 
			
		||||
			} else if ( element.nodeType === Node.COMMENT_NODE ) {
 | 
			
		||||
 | 
			
		||||
				return;
 | 
			
		||||
 | 
			
		||||
			} else if ( element instanceof HTMLCanvasElement ) {
 | 
			
		||||
 | 
			
		||||
				// Canvas element
 | 
			
		||||
				if ( element.style.display === 'none' ) return;
 | 
			
		||||
 | 
			
		||||
				const rect = element.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
				x = rect.left - offset.left - 0.5;
 | 
			
		||||
				y = rect.top - offset.top - 0.5;
 | 
			
		||||
 | 
			
		||||
			        context.save();
 | 
			
		||||
				const dpr = window.devicePixelRatio;
 | 
			
		||||
				context.scale( 1 / dpr, 1 / dpr );
 | 
			
		||||
				context.drawImage( element, x, y );
 | 
			
		||||
				context.restore();
 | 
			
		||||
 | 
			
		||||
			} else if ( element instanceof HTMLImageElement ) {
 | 
			
		||||
 | 
			
		||||
				if ( element.style.display === 'none' ) return;
 | 
			
		||||
 | 
			
		||||
				const rect = element.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
				x = rect.left - offset.left - 0.5;
 | 
			
		||||
				y = rect.top - offset.top - 0.5;
 | 
			
		||||
				width = rect.width;
 | 
			
		||||
				height = rect.height;
 | 
			
		||||
 | 
			
		||||
				context.drawImage( element, x, y, width, height );
 | 
			
		||||
 | 
			
		||||
			} else {
 | 
			
		||||
 | 
			
		||||
				if ( element.style.display === 'none' ) return;
 | 
			
		||||
 | 
			
		||||
				const rect = element.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
				x = rect.left - offset.left - 0.5;
 | 
			
		||||
				y = rect.top - offset.top - 0.5;
 | 
			
		||||
				width = rect.width;
 | 
			
		||||
				height = rect.height;
 | 
			
		||||
 | 
			
		||||
				style = window.getComputedStyle( element );
 | 
			
		||||
 | 
			
		||||
				// Get the border of the element used for fill and border
 | 
			
		||||
 | 
			
		||||
				buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) );
 | 
			
		||||
 | 
			
		||||
				const backgroundColor = style.backgroundColor;
 | 
			
		||||
 | 
			
		||||
				if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {
 | 
			
		||||
 | 
			
		||||
					context.fillStyle = backgroundColor;
 | 
			
		||||
					context.fill();
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// If all the borders match then stroke the round rectangle
 | 
			
		||||
 | 
			
		||||
				const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ];
 | 
			
		||||
 | 
			
		||||
				let match = true;
 | 
			
		||||
				let prevBorder = null;
 | 
			
		||||
 | 
			
		||||
				for ( const border of borders ) {
 | 
			
		||||
 | 
			
		||||
					if ( prevBorder !== null ) {
 | 
			
		||||
 | 
			
		||||
						match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) &&
 | 
			
		||||
						( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) &&
 | 
			
		||||
						( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] );
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if ( match === false ) break;
 | 
			
		||||
 | 
			
		||||
					prevBorder = border;
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if ( match === true ) {
 | 
			
		||||
 | 
			
		||||
					// They all match so stroke the rectangle from before allows for border-radius
 | 
			
		||||
 | 
			
		||||
					const width = parseFloat( style.borderTopWidth );
 | 
			
		||||
 | 
			
		||||
					if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) {
 | 
			
		||||
 | 
			
		||||
						context.strokeStyle = style.borderTopColor;
 | 
			
		||||
						context.lineWidth = width;
 | 
			
		||||
						context.stroke();
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				} else {
 | 
			
		||||
 | 
			
		||||
					// Otherwise draw individual borders
 | 
			
		||||
 | 
			
		||||
					drawBorder( style, 'borderTop', x, y, width, 0 );
 | 
			
		||||
					drawBorder( style, 'borderLeft', x, y, 0, height );
 | 
			
		||||
					drawBorder( style, 'borderBottom', x, y + height, width, 0 );
 | 
			
		||||
					drawBorder( style, 'borderRight', x + width, y, 0, height );
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if ( element instanceof HTMLInputElement ) {
 | 
			
		||||
 | 
			
		||||
					let accentColor = style.accentColor;
 | 
			
		||||
 | 
			
		||||
					if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color;
 | 
			
		||||
 | 
			
		||||
					color.set( accentColor );
 | 
			
		||||
 | 
			
		||||
					const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) );
 | 
			
		||||
					const accentTextColor = luminance < 0.5 ? 'white' : '#111111';
 | 
			
		||||
 | 
			
		||||
					if ( element.type === 'radio' ) {
 | 
			
		||||
 | 
			
		||||
						buildRectPath( x, y, width, height, height );
 | 
			
		||||
 | 
			
		||||
						context.fillStyle = 'white';
 | 
			
		||||
						context.strokeStyle = accentColor;
 | 
			
		||||
						context.lineWidth = 1;
 | 
			
		||||
						context.fill();
 | 
			
		||||
						context.stroke();
 | 
			
		||||
 | 
			
		||||
						if ( element.checked ) {
 | 
			
		||||
 | 
			
		||||
							buildRectPath( x + 2, y + 2, width - 4, height - 4, height );
 | 
			
		||||
 | 
			
		||||
							context.fillStyle = accentColor;
 | 
			
		||||
							context.strokeStyle = accentTextColor;
 | 
			
		||||
							context.lineWidth = 2;
 | 
			
		||||
							context.fill();
 | 
			
		||||
							context.stroke();
 | 
			
		||||
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if ( element.type === 'checkbox' ) {
 | 
			
		||||
 | 
			
		||||
						buildRectPath( x, y, width, height, 2 );
 | 
			
		||||
 | 
			
		||||
						context.fillStyle = element.checked ? accentColor : 'white';
 | 
			
		||||
						context.strokeStyle = element.checked ? accentTextColor : accentColor;
 | 
			
		||||
						context.lineWidth = 1;
 | 
			
		||||
						context.stroke();
 | 
			
		||||
						context.fill();
 | 
			
		||||
 | 
			
		||||
						if ( element.checked ) {
 | 
			
		||||
 | 
			
		||||
							const currentTextAlign = context.textAlign;
 | 
			
		||||
 | 
			
		||||
							context.textAlign = 'center';
 | 
			
		||||
 | 
			
		||||
							const properties = {
 | 
			
		||||
								color: accentTextColor,
 | 
			
		||||
								fontFamily: style.fontFamily,
 | 
			
		||||
								fontSize: height + 'px',
 | 
			
		||||
								fontWeight: 'bold'
 | 
			
		||||
							};
 | 
			
		||||
 | 
			
		||||
							drawText( properties, x + ( width / 2 ), y, '✔' );
 | 
			
		||||
 | 
			
		||||
							context.textAlign = currentTextAlign;
 | 
			
		||||
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if ( element.type === 'range' ) {
 | 
			
		||||
 | 
			
		||||
						const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) );
 | 
			
		||||
						const position = ( ( value - min ) / ( max - min ) ) * ( width - height );
 | 
			
		||||
 | 
			
		||||
						buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 );
 | 
			
		||||
						context.fillStyle = accentTextColor;
 | 
			
		||||
						context.strokeStyle = accentColor;
 | 
			
		||||
						context.lineWidth = 1;
 | 
			
		||||
						context.fill();
 | 
			
		||||
						context.stroke();
 | 
			
		||||
 | 
			
		||||
						buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 );
 | 
			
		||||
						context.fillStyle = accentColor;
 | 
			
		||||
						context.fill();
 | 
			
		||||
 | 
			
		||||
						buildRectPath( x + position, y, height, height, height / 2 );
 | 
			
		||||
						context.fillStyle = accentColor;
 | 
			
		||||
						context.fill();
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if ( element.type === 'color' || element.type === 'text' || element.type === 'number' ) {
 | 
			
		||||
 | 
			
		||||
						clipper.add( { x: x, y: y, width: width, height: height } );
 | 
			
		||||
 | 
			
		||||
						drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), element.value );
 | 
			
		||||
 | 
			
		||||
						clipper.remove();
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			/*
 | 
			
		||||
			// debug
 | 
			
		||||
			context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 );
 | 
			
		||||
			context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 );
 | 
			
		||||
			*/
 | 
			
		||||
 | 
			
		||||
			const isClipping = style.overflow === 'auto' || style.overflow === 'hidden';
 | 
			
		||||
 | 
			
		||||
			if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );
 | 
			
		||||
 | 
			
		||||
			for ( let i = 0; i < element.childNodes.length; i ++ ) {
 | 
			
		||||
 | 
			
		||||
				drawElement( element.childNodes[ i ], style );
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ( isClipping ) clipper.remove();
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const offset = element.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
		let canvas = canvases.get( element );
 | 
			
		||||
 | 
			
		||||
		if ( canvas === undefined ) {
 | 
			
		||||
 | 
			
		||||
			canvas = document.createElement( 'canvas' );
 | 
			
		||||
			canvas.width = offset.width;
 | 
			
		||||
			canvas.height = offset.height;
 | 
			
		||||
			canvases.set( element, canvas );
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const context = canvas.getContext( '2d'/*, { alpha: false }*/ );
 | 
			
		||||
 | 
			
		||||
		const clipper = new Clipper( context );
 | 
			
		||||
 | 
			
		||||
		// console.time( 'drawElement' );
 | 
			
		||||
 | 
			
		||||
		context.clearRect(0, 0, canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
		drawElement( element );
 | 
			
		||||
 | 
			
		||||
		// console.timeEnd( 'drawElement' );
 | 
			
		||||
 | 
			
		||||
		return canvas;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function htmlevent( element, event, x, y ) {
 | 
			
		||||
 | 
			
		||||
		const mouseEventInit = {
 | 
			
		||||
			clientX: ( x * element.offsetWidth ) + element.offsetLeft,
 | 
			
		||||
			clientY: ( y * element.offsetHeight ) + element.offsetTop,
 | 
			
		||||
			view: element.ownerDocument.defaultView
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// TODO: Find out why this is added. Keep commented out when this file is updated
 | 
			
		||||
		// window.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
 | 
			
		||||
 | 
			
		||||
		const rect = element.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
		x = x * rect.width + rect.left;
 | 
			
		||||
		y = y * rect.height + rect.top;
 | 
			
		||||
 | 
			
		||||
		function traverse( element ) {
 | 
			
		||||
 | 
			
		||||
			if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) {
 | 
			
		||||
 | 
			
		||||
				const rect = element.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
				if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) {
 | 
			
		||||
 | 
			
		||||
					element.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
 | 
			
		||||
 | 
			
		||||
					if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) {
 | 
			
		||||
 | 
			
		||||
						const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) );
 | 
			
		||||
 | 
			
		||||
						const width = rect.width;
 | 
			
		||||
						const offsetX = x - rect.x;
 | 
			
		||||
						const proportion = offsetX / width;
 | 
			
		||||
						element.value = min + ( max - min ) * proportion;
 | 
			
		||||
						element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				for ( let i = 0; i < element.childNodes.length; i ++ ) {
 | 
			
		||||
 | 
			
		||||
					traverse( element.childNodes[ i ] );
 | 
			
		||||
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		traverse( element );
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/* jshint esversion: 9, -W097 */
 | 
			
		||||
 | 
			
		||||
	const schemaHTML = {
 | 
			
		||||
		html: {
 | 
			
		||||
			type: 'selector',
 | 
			
		||||
		},
 | 
			
		||||
		cursor: {
 | 
			
		||||
			type: 'selector',
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		schemaHTML.html.description = `HTML element to use.`;
 | 
			
		||||
		schemaHTML.cursor.description = `Visual indicator for where the user is currently pointing`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const _pointer = new THREE.Vector2();
 | 
			
		||||
	const _event = { type: '', data: _pointer };
 | 
			
		||||
	AFRAME.registerComponent('html', {
 | 
			
		||||
		schema: schemaHTML,
 | 
			
		||||
		init() {
 | 
			
		||||
			this.rerender = this.rerender.bind(this);
 | 
			
		||||
			this.handle = this.handle.bind(this);
 | 
			
		||||
			this.onClick = e => this.handle('click', e);
 | 
			
		||||
			this.onMouseLeave = e => this.handle('mouseleave', e);
 | 
			
		||||
			this.onMouseEnter = e => this.handle('mouseenter', e);
 | 
			
		||||
			this.onMouseUp = e => this.handle('mouseup', e);
 | 
			
		||||
			this.onMouseDown = e => this.handle('mousedown', e);
 | 
			
		||||
			this.mouseMoveDetail = {
 | 
			
		||||
				detail: {
 | 
			
		||||
					cursorEl: null,
 | 
			
		||||
					intersection: null
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
		play() {
 | 
			
		||||
			this.el.addEventListener('click', this.onClick);
 | 
			
		||||
			this.el.addEventListener('mouseleave', this.onMouseLeave);
 | 
			
		||||
			this.el.addEventListener('mouseenter', this.onMouseEnter);
 | 
			
		||||
			this.el.addEventListener('mouseup', this.onMouseUp);
 | 
			
		||||
			this.el.addEventListener('mousedown', this.onMouseDown);
 | 
			
		||||
		},
 | 
			
		||||
		pause() {
 | 
			
		||||
			this.el.removeEventListener('click', this.onClick);
 | 
			
		||||
			this.el.removeEventListener('mouseleave', this.onMouseLeave);
 | 
			
		||||
			this.el.removeEventListener('mouseenter', this.onMouseEnter);
 | 
			
		||||
			this.el.removeEventListener('mouseup', this.onMouseUp);
 | 
			
		||||
			this.el.removeEventListener('mousedown', this.onMouseDown);
 | 
			
		||||
		},
 | 
			
		||||
		update() {
 | 
			
		||||
			this.remove();
 | 
			
		||||
			if (!this.data.html) return;
 | 
			
		||||
			const mesh = new HTMLMesh(this.data.html);
 | 
			
		||||
			this.el.setObject3D('html', mesh);
 | 
			
		||||
			this.data.html.addEventListener('input', this.rerender);
 | 
			
		||||
			this.data.html.addEventListener('change', this.rerender);
 | 
			
		||||
			this.cursor = this.data.cursor ? this.data.cursor.object3D : null;
 | 
			
		||||
		},
 | 
			
		||||
		tick() {
 | 
			
		||||
			if (this.activeRaycaster) {
 | 
			
		||||
				const intersection = this.activeRaycaster.components.raycaster.getIntersection(this.el);
 | 
			
		||||
				this.mouseMoveDetail.detail.cursorEl = this.activeRaycaster;
 | 
			
		||||
				this.mouseMoveDetail.detail.intersection = intersection;
 | 
			
		||||
				this.handle('mousemove', this.mouseMoveDetail);
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		handle(type, evt) {
 | 
			
		||||
			const intersection = evt.detail.intersection;
 | 
			
		||||
			const raycaster = evt.detail.cursorEl;
 | 
			
		||||
			if (type === 'mouseenter') {
 | 
			
		||||
				this.activeRaycaster = raycaster;
 | 
			
		||||
			}
 | 
			
		||||
			if (type === 'mouseleave' && this.activeRaycaster === raycaster) {
 | 
			
		||||
				this.activeRaycaster = null;
 | 
			
		||||
			}
 | 
			
		||||
			if (this.cursor) this.cursor.visible = false;
 | 
			
		||||
			if (intersection) {
 | 
			
		||||
				const mesh = this.el.getObject3D('html');
 | 
			
		||||
				const uv = intersection.uv;
 | 
			
		||||
				_event.type = type;
 | 
			
		||||
				_event.data.set( uv.x, 1 - uv.y );
 | 
			
		||||
				mesh.dispatchEvent( _event );
 | 
			
		||||
 | 
			
		||||
				if (this.cursor) {
 | 
			
		||||
					this.cursor.visible = true;
 | 
			
		||||
					this.cursor.parent.worldToLocal(this.cursor.position.copy(intersection.point));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		rerender() {
 | 
			
		||||
			const mesh = this.el.getObject3D('html');
 | 
			
		||||
			if (mesh && !mesh.material.map.scheduleUpdate) {
 | 
			
		||||
				mesh.material.map.scheduleUpdate = setTimeout( () => mesh.material.map.update(), 16 );
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		remove() {
 | 
			
		||||
			const mesh = this.el.getObject3D('html');
 | 
			
		||||
			if (mesh) {
 | 
			
		||||
				this.el.removeObject3D('html');
 | 
			
		||||
				this.data.html.removeEventListener('input', this.rerender);
 | 
			
		||||
				this.data.html.removeEventListener('change', this.rerender);
 | 
			
		||||
				mesh.dispose();
 | 
			
		||||
			}
 | 
			
		||||
			this.activeRaycaster = null;
 | 
			
		||||
			this.mouseMoveDetail.detail.cursorEl = null;
 | 
			
		||||
			this.mouseMoveDetail.detail.intersection = null;
 | 
			
		||||
			this.cursor = null;
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
})(THREE);
 | 
			
		||||
//# sourceMappingURL=aframe-html.js.map
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,9 @@ window.AFRAME.registerComponent('pinch-to-teleport', {
 | 
			
		|||
      pos.x += direction.x 
 | 
			
		||||
      pos.z += direction.z
 | 
			
		||||
      // set the new position
 | 
			
		||||
      this.data.rig.setAttribute("position", pos);
 | 
			
		||||
      if( !this.data.rig ){
 | 
			
		||||
        this.data.rig.setAttribute("position", pos);
 | 
			
		||||
      }
 | 
			
		||||
      // !!! NOTE - it would be more efficient to do the
 | 
			
		||||
      // position change on the players THREE.Object:
 | 
			
		||||
      // `player.object3D.position.add(direction)`
 | 
			
		||||
| 
						 | 
				
			
			@ -2,19 +2,23 @@
 | 
			
		|||
 | 
			
		||||
AFRAME.registerComponent('pressable', {
 | 
			
		||||
    schema: {
 | 
			
		||||
        pressDistance: {
 | 
			
		||||
            default: 0.01
 | 
			
		||||
        }
 | 
			
		||||
      pressDistance: { default: 0.005 },
 | 
			
		||||
      pressDuration: { default: 300 },
 | 
			
		||||
      immersiveOnly: { default: true } 
 | 
			
		||||
    },
 | 
			
		||||
    init: function() {
 | 
			
		||||
        this.worldPosition = new THREE.Vector3();
 | 
			
		||||
        this.fingerWorldPosition = new THREE.Vector3();
 | 
			
		||||
        this.raycaster = new THREE.Raycaster()
 | 
			
		||||
        this.handEls = document.querySelectorAll('[hand-tracking-controls]');
 | 
			
		||||
        this.pressed = false;
 | 
			
		||||
        this.distance = -1
 | 
			
		||||
        // we throttle by distance, to support scenes with loads of clickable objects (far away)
 | 
			
		||||
        this.tick = this.throttleByDistance( () => this.detectPress() )
 | 
			
		||||
        this.el.addEventListener("raycaster-intersected", (e) => {
 | 
			
		||||
          if( !this.data || (this.data.immersiveOnly && !this.el.sceneEl.renderer.xr.isPresenting) ) return
 | 
			
		||||
          this.el.emit('click', e.detail ) 
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    throttleByDistance: function(f){
 | 
			
		||||
        return function(){
 | 
			
		||||
| 
						 | 
				
			
			@ -30,43 +34,48 @@ AFRAME.registerComponent('pressable', {
 | 
			
		|||
        }
 | 
			
		||||
    },
 | 
			
		||||
    detectPress: function(){
 | 
			
		||||
        if( this.handEls.length == 0 ){
 | 
			
		||||
            this.handEls = document.querySelectorAll('[hand-tracking-controls]');
 | 
			
		||||
        }
 | 
			
		||||
        var handEls = this.handEls;
 | 
			
		||||
        var handEl;
 | 
			
		||||
        let minDistance = 5
 | 
			
		||||
 | 
			
		||||
        // compensate for xrf-get AFRAME component (which references non-reparented buffergeometries from the 3D model)
 | 
			
		||||
        let object3D = this.el.object3D.child || this.el.object3D
 | 
			
		||||
        // compensate for an object inside a group 
 | 
			
		||||
        let object3D = this.el.object3D.type == "Group" ? this.el.object3D.children[0] : this.el.object3D
 | 
			
		||||
        if( !object3D ) return
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < handEls.length; i++) {
 | 
			
		||||
            handEl = handEls[i];
 | 
			
		||||
            let indexTipPosition  = handEl.components['hand-tracking-controls'].indexTipPosition
 | 
			
		||||
            // Apply the relative position to the parent's world position 
 | 
			
		||||
            handEl.object3D.updateMatrixWorld();
 | 
			
		||||
            handEl.object3D.getWorldPosition( this.fingerWorldPosition )
 | 
			
		||||
            this.fingerWorldPosition.add( indexTipPosition )
 | 
			
		||||
            let indexTip  = handEl.object3D.getObjectByName('index-finger-tip')
 | 
			
		||||
            if( ! indexTip ) return // nothing to do here 
 | 
			
		||||
 | 
			
		||||
            this.raycaster.far = this.data.pressDistance
 | 
			
		||||
            // Create a direction vector (doesnt matter because it is supershort for 'touch' purposes)
 | 
			
		||||
            const direction = new THREE.Vector3(1.0,0,0);
 | 
			
		||||
            this.raycaster.set(this.fingerWorldPosition, direction)
 | 
			
		||||
 | 
			
		||||
            // Create a direction vector to negative Z
 | 
			
		||||
            const direction = new THREE.Vector3(0,0,-1.0);
 | 
			
		||||
            direction.normalize()
 | 
			
		||||
            this.raycaster.set(indexTip.position, direction)
 | 
			
		||||
            intersects = this.raycaster.intersectObjects([object3D],true)
 | 
			
		||||
 | 
			
		||||
            object3D.getWorldPosition(this.worldPosition)
 | 
			
		||||
      
 | 
			
		||||
            distance    = this.fingerWorldPosition.distanceTo(this.worldPosition)
 | 
			
		||||
            distance    = indexTip.position.distanceTo(this.worldPosition)
 | 
			
		||||
            minDistance = distance < minDistance ? distance : minDistance 
 | 
			
		||||
 | 
			
		||||
            if (intersects.length ){
 | 
			
		||||
              this.i = this.i || 0;
 | 
			
		||||
              if( !this.pressed ){
 | 
			
		||||
                this.el.emit('pressedstarted');
 | 
			
		||||
                this.el.emit('click');
 | 
			
		||||
                this.el.emit('pressedstarted', intersects);
 | 
			
		||||
                this.el.emit('click', intersects);
 | 
			
		||||
                this.pressed = setTimeout( () => {
 | 
			
		||||
                  this.el.emit('pressedended');
 | 
			
		||||
                  this.el.emit('pressedended', intersects);
 | 
			
		||||
                  this.pressed = null 
 | 
			
		||||
                },300)
 | 
			
		||||
                }, this.data.pressDuration )
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.distance = minDistance
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
/**
 | 
			
		||||
 * ## [require](com/require('').js)
 | 
			
		||||
 *
 | 
			
		||||
 * automatically requires dependencies 
 | 
			
		||||
 * automatically requires dependencies or missing components
 | 
			
		||||
 * 
 | 
			
		||||
 * ```javascript
 | 
			
		||||
 * await AFRAME.utils.require( this.dependencies )               (*) autoload missing components
 | 
			
		||||
| 
						 | 
				
			
			@ -74,3 +74,38 @@ AFRAME.utils.require = function(arr_or_obj,opts){
 | 
			
		|||
AFRAME.utils.require.required   = {}
 | 
			
		||||
 | 
			
		||||
AFRAME.utils.require.baseURL = './com/'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
//// this component will scan the DOM for missing components and lazy load them
 | 
			
		||||
//AFRAME.registerSystem('require',{
 | 
			
		||||
//
 | 
			
		||||
//  init: function(){
 | 
			
		||||
//    this.components = []
 | 
			
		||||
//    // observe HTML changes in <a-scene>
 | 
			
		||||
//    observer = new MutationObserver( (a,b) => this.getMissingComponents(a,b) )
 | 
			
		||||
//    observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
 | 
			
		||||
//  },
 | 
			
		||||
//
 | 
			
		||||
//  getMissingComponents: function(mutationsList,observer){
 | 
			
		||||
//    let els         = [...this.sceneEl.getElementsByTagName("*")]
 | 
			
		||||
//    let seen        = []
 | 
			
		||||
//
 | 
			
		||||
//    els.map( async (el) => {
 | 
			
		||||
//      let attrs    = el.getAttributeNames()
 | 
			
		||||
//                       .filter( (a) => a.match(/(^aframe-injected|^data-aframe|^id$|^class$|^on)/) ? null : a )
 | 
			
		||||
//      for( let attr in attrs ){
 | 
			
		||||
//        let component = attrs[attr]
 | 
			
		||||
//        if( el.components && !el.components[component] ){
 | 
			
		||||
//          console.info(`require.js: lazy-loading missing <${el.tagName.toLowerCase()} ${component} ... > (TODO: fix selectors in schema)`)
 | 
			
		||||
//          // require && remount
 | 
			
		||||
//          try{
 | 
			
		||||
//            await AFRAME.utils.require([component])
 | 
			
		||||
//            el.removeAttribute(component)
 | 
			
		||||
//            el.setAttribute(component, el.getAttribute(component) )
 | 
			
		||||
//          }catch(e){ } // give up, normal AFRAME behaviour follows
 | 
			
		||||
//        }
 | 
			
		||||
//      }
 | 
			
		||||
//    })
 | 
			
		||||
//  }
 | 
			
		||||
//
 | 
			
		||||
//})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										56
									
								
								com/wearable.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								com/wearable.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,56 @@
 | 
			
		|||
AFRAME.registerComponent('wearable', {
 | 
			
		||||
  schema:{
 | 
			
		||||
    el: {type:"selector"},
 | 
			
		||||
    controlPos: {type:"vec3"}, 
 | 
			
		||||
    controlRot: {type:"vec3"},
 | 
			
		||||
    handPos: {type:"vec3"}, 
 | 
			
		||||
    handRot: {type:"vec3"} 
 | 
			
		||||
  }, 
 | 
			
		||||
  init: function(){
 | 
			
		||||
    this.remember()
 | 
			
		||||
 | 
			
		||||
    if( !this.data.el ) return console.warn(`wear.js: cannot find ${this.data.el}`)
 | 
			
		||||
    this.data.el.object3D.name = 'wearable'
 | 
			
		||||
 | 
			
		||||
    // hand vs controller multi attach-heuristics (intended to survived AFRAME updates)
 | 
			
		||||
    this.data.el.addEventListener('controllermodelready',  this.attachWhatEverController.bind(this) ) // downside: no model yet
 | 
			
		||||
    this.data.el.addEventListener('model-loaded',          this.attachWhatEverController.bind(this) ) // downside: only called once [model not added yet]
 | 
			
		||||
    this.el.sceneEl.addEventListener('controllersupdated', this.attachWhatEverController.bind(this) ) // downside: only called when switching [no model yet]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  attachWhatEverController: function(e){
 | 
			
		||||
    setTimeout( () => { // needed because the events are called before the model was added via add())
 | 
			
		||||
      let wrist = false 
 | 
			
		||||
      let hand  =  this.data.el.components['hand-tracking-controls']
 | 
			
		||||
      if( hand && hand.controllerPresent ) wrist = hand.wristObject3D
 | 
			
		||||
      this.attach( wrist || this.data.el.object3D)
 | 
			
		||||
      this.update( wrist ? 'hand' : 'control')
 | 
			
		||||
    },100)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  attach: function(target){
 | 
			
		||||
    if( this.target && target.uuid == this.target.uuid ) return// already attached 
 | 
			
		||||
    target.add(this.el.object3D )
 | 
			
		||||
    this.target = target
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  detach: function(){
 | 
			
		||||
    this.parent.add(this.el.object3D) 
 | 
			
		||||
    this.el.object3D.position.copy( this.position )
 | 
			
		||||
    this.el.object3D.rotation.copy( this.rotation )
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  remember: function(){
 | 
			
		||||
    this.position = this.el.object3D.position.clone()
 | 
			
		||||
    this.rotation = this.el.object3D.rotation.clone()
 | 
			
		||||
    this.parent   = this.el.object3D.parent
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  update: function(type){
 | 
			
		||||
    let position = type == 'hand' ? this.data.handPos : this.data.controlPos
 | 
			
		||||
    let rotation = type == 'hand' ? this.data.handRot : this.data.controlRot
 | 
			
		||||
    this.el.object3D.position.copy( position )
 | 
			
		||||
    this.el.object3D.rotation.set( rotation.x, rotation.y, rotation.z )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -37,6 +37,7 @@ AFRAME.registerComponent('window', {
 | 
			
		|||
    x:         {type:'string',"default":"center"},
 | 
			
		||||
    y:         {type:'string',"default":"center"},
 | 
			
		||||
    "class":   {type:'array',"default":[]},
 | 
			
		||||
    autoresize:{type:'bool', "default": false}
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  dependencies:{
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +85,7 @@ AFRAME.registerComponent('window', {
 | 
			
		|||
        this.el.emit('window.oncreate',{})
 | 
			
		||||
        // resize after the dom content has been rendered & updated 
 | 
			
		||||
        setTimeout( () => {
 | 
			
		||||
          if( !this.data.max ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
 | 
			
		||||
          if( !this.data.max && this.data.autoresize ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
 | 
			
		||||
          // hint grabbable's obb-collider to track the window-object
 | 
			
		||||
          this.el.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
 | 
			
		||||
          this.el.components['obb-collider'].update()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue