diff --git a/app/apptiler.js b/app/apptiler.js
new file mode 100644
index 0000000..73bf23f
--- /dev/null
+++ b/app/apptiler.js
@@ -0,0 +1,289 @@
+//
+// this is just an AFRAME wrapper for golden-layout v2 (docs: https://golden-layout.github.io/golden-layout/)
+//
+//
+AFRAME.registerComponent('apptiler', {
+ schema: {
+ },
+
+ requires:{
+ "goldenlayout_css1": "https://unpkg.com/golden-layout@2.6.0/dist/css/goldenlayout-base.css",
+ "goldenlayout_css2": "https://unpkg.com/golden-layout@2.6.0/dist/css/themes/goldenlayout-dark-theme.css"
+ },
+
+ dom: {
+ events: [],
+
+ // for stylesheet see bottom of file
+ html: (me) => `
+
`,
+
+ },
+
+ events:{
+ DOMready: async function(){
+ await this.initLayout(this)
+ AFRAME.app.foreach( (opts) => {
+ this.add( opts.component, opts.app.el.dom)
+ if( opts.component != 'apptiler' ) opts.app.el.dom.querySelector('.modal').classList.add(['tile'])
+ })
+ setTimeout( () => document.querySelector('#overlay').classList.add(['apptiler']), 100 )
+ },
+
+ },
+
+ init: function () {
+ this.require( this.requires )
+ },
+
+ initLayout: async function(){
+ if( this.goldenLayout !== undefined || !this.el.dom.querySelector(".modals")) return console.warn("TODO: fix duplicate ready-events")
+
+ let { GoldenLayout } = await import("https://cdn.skypack.dev/pin/golden-layout@v2.5.0-dAz3xMzxIRpbnbfEAik0/mode=imports/optimized/golden-layout.js");
+
+ class Modal {
+ constructor(container) {
+ this.container = container;
+ this.rootElement = container.element;
+ this.rootElement.innerHTML = ''
+ this.resizeWithContainerAutomatically = true;
+ }
+ }
+ const myLayout = {
+ root: {
+ type: 'row',
+ content: []
+ }
+ };
+
+ this.goldenLayout = new GoldenLayout( this.el.dom.querySelector('.modals'));
+ this.goldenLayout.registerComponent('Modal', Modal);
+ this.goldenLayout.loadLayout(myLayout);
+ },
+
+ add: function(title,el){
+ if( title == 'apptiler' ) return // dont add yourself to yourself please
+ let item = this.goldenLayout.addComponent('Modal', undefined, title )
+ try{
+ item.parentItem.contentItems[ item.parentItem.contentItems.length-1 ].element.querySelector('.lm_content').appendChild(el)
+ }catch(e){} // ignore elements which are already appended
+ },
+
+ manifest: { // HTML5 manifest to identify app to xrsh
+ "short_name": "windowmanager",
+ "name": "Window Manager",
+ "icons": [
+ {
+ "src": "/images/icons-vector.svg",
+ "type": "image/svg+xml",
+ "sizes": "512x512"
+ }
+ ],
+ "id": "/?source=pwa",
+ "start_url": "/?source=pwa",
+ "background_color": "#3367D6",
+ "display": "standalone",
+ "scope": "/",
+ "theme_color": "#3367D6",
+ "shortcuts": [],
+ "description": "2D/3D management of windows",
+ "screenshots": [
+ {
+ "src": "/images/screenshot1.png",
+ "type": "image/png",
+ "sizes": "540x720",
+ "form_factor": "narrow"
+ }
+ ],
+ "help":`
+Window Manager
+
+The window manager manages all the windows in 2D/XR.
+This is a core XRSH system application
+ `
+ }
+
+});
+
+document.head.innerHTML += `
+
+`
diff --git a/app/helloworld.js b/app/helloworld.js
index d9fed91..56da0db 100644
--- a/app/helloworld.js
+++ b/app/helloworld.js
@@ -8,18 +8,13 @@ AFRAME.registerComponent('helloworld', {
"stylis": "https://unpkg.com/stylis@4.3.1/dist/umd/stylis.js" // modern CSS (https://stylis.js.org)
},
- dependencies: ['windowmanager'],
-
dom: {
- scale: 3.5,
- modal: true,
+ scale: 3,
events: ['click','input'],
html: (me) => `
`,
css: `.modal.hello {
- position:relative;top:0;width:200px
- foo { /* modern css supported via stylis */ }
+ position:relative;
+ top:0;
+ width:200px;
+ .title { font-weight:bold; } /* modern nested buildless css thanks to stylis */
}`
},
events:{
- html: function( ){
- console.log("html-mesh mounted")
- this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`)
- }, // html-component was added to this AFRAME entity
+
+ html: function( ){ console.log("html-mesh requirement mounted") },
+ stylis: function( ){ console.log("stylis requirement mounted") },
+
+ DOMready: function(e){
+ // our reactive dom element has been added to the dom (DOMElement = this.el.dom)
+ },
+
click: function(e){ // a click was detected on this.el.dom or AFRAME entity
let el = e.detail.target || e.detail.srcElement
if( !el ) return
if( el.className.match("fold") ) this.el.toggleFold()
if( el.className.match("close") ) this.el.close()
},
+
input: function(e){
if( !e.detail.target ) return
if( e.detail.target.id == 'myRange' ) this.data.value = e.detail.target.value // reactive demonstration
},
- value: function(e){ this.el.dom.querySelector("#value").innerHTML = e.detail.v }, // this.data.title was changed
- ready: function(){
- }
+
+ value: function(e){ this.el.dom.querySelector("#value").innerHTML = e.detail.v }, // auto-emitted when this.data.value gets updated
+
},
init: function () {
this.require( this.requires )
+
+ this.scene.addEventListener('apps:2D', () => this.el.setAttribute('visible', false) )
+ this.scene.addEventListener('apps:XR', () => {
+ this.el.setAttribute('visible', true)
+ this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`)
+ })
},
manifest: { // HTML5 manifest to identify app to xrsh
diff --git a/app/windowmanager.js b/app/windowmanager.js
deleted file mode 100644
index 606db5d..0000000
--- a/app/windowmanager.js
+++ /dev/null
@@ -1,416 +0,0 @@
-//
-// this is just an AFRAME wrapper for golden-layout v2 (docs: https://golden-layout.github.io/golden-layout/)
-//
-//
-
-AFRAME.registerComponent('windowmanager', {
- schema: {
- },
-
- requires:{
- "goldenlayout_css1": "https://unpkg.com/golden-layout@2.6.0/dist/css/goldenlayout-base.css",
- "goldenlayout_css2": "https://unpkg.com/golden-layout@2.6.0/dist/css/themes/goldenlayout-dark-theme.css"
- },
-
- dom: {
- events: [],
-
- // for stylesheet see bottom of file
- html: (me) => `
- `,
-
- },
-
- events:{
- ready: function(){
- this.initLayout(this)
- }
- },
-
- init: function () {
- this.require( this.requires )
- },
-
- initLayout: async function(){
- if( this.goldenLayout !== undefined || !this.el.dom.querySelector(".modals")) return console.warn("TODO: fix duplicate ready-events")
-
- let { GoldenLayout } = await import("https://cdn.skypack.dev/pin/golden-layout@v2.5.0-dAz3xMzxIRpbnbfEAik0/mode=imports/optimized/golden-layout.js");
-
- class Modal {
- constructor(container) {
- this.container = container;
- this.rootElement = container.element;
- this.rootElement.innerHTML = ''
- this.resizeWithContainerAutomatically = true;
- }
- }
- const myLayout = {
- root: {
- type: 'row',
- content: [
- {
- title: 'Terminal',
- type: 'component',
- componentType: 'Modal',
- width: 50,
- },
- {
- title: 'Welcome to XR shell',
- type: 'component',
- componentType: 'Modal',
- // componentState: { text: 'Component 2' }
- }
- ]
- }
- };
-
- this.goldenLayout = new GoldenLayout( this.el.dom.querySelector('.modals'));
- this.goldenLayout.registerComponent('Modal', Modal);
- this.goldenLayout.loadLayout(myLayout);
-
- },
-
- add: function(title,el){
- setTimeout( () => {
- let item = this.goldenLayout.addComponent('Modal', undefined, title )
- console.dir(item)
- item.parentItem.contentItems[ item.parentItem.contentItems.length-1 ].element.querySelector('.lm_content').appendChild(el)
- },1000)
- },
-
- manifest: { // HTML5 manifest to identify app to xrsh
- "short_name": "windowmanager",
- "name": "Window Manager",
- "icons": [
- {
- "src": "/images/icons-vector.svg",
- "type": "image/svg+xml",
- "sizes": "512x512"
- }
- ],
- "id": "/?source=pwa",
- "start_url": "/?source=pwa",
- "background_color": "#3367D6",
- "display": "standalone",
- "scope": "/",
- "theme_color": "#3367D6",
- "shortcuts": [],
- "description": "2D/3D management of windows",
- "screenshots": [
- {
- "src": "/images/screenshot1.png",
- "type": "image/png",
- "sizes": "540x720",
- "form_factor": "narrow"
- }
- ],
- "help":`
-Window Manager
-
-The window manager manages all the windows in 2D/XR.
-This is a core XRSH system application
- `
- }
-
-});
-
-// monkeypatching updateProperties will detect a component config like:
-// dom: {
-// html: `Welcome to XR shell
`,
-// css: `#hello {color:red}`,
-// events: ['click']
-// }
-// and use it to create a reactive DOM-component (using native javascript Proxy)
-// which delegates all related DOM-events AND data-changes back to the AFRAME component
-AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
- return function(){
- updateProperties.apply(this,arguments)
- if( this.dom && this.data && this.data.uri ){
-
- tasks = {
-
- generateUniqueId: () => {
- this.el.uid = String(Math.random()).substr(2)
- return tasks
- },
-
- ensureOverlay: () => {
- let overlay = document.querySelector('#overlay')
- if( !overlay ){
- overlay = document.createElement('div')
- overlay.id = "overlay"
- document.body.appendChild(overlay)
- document.querySelector("a-scene").setAttribute("webxr","overlayElement:#overlay")
- }
- tasks.overlay = overlay
- return tasks
- },
-
- createReactiveDOMElement: () => {
- const reactify = (el,aframe) => new Proxy(this.data,{
- get(me,k,v) { return me[k] },
- set(me,k,v){
- me[k] = v
- aframe.emit(k,{el,k,v})
- }
- })
- this.el.dom = document.createElement('div')
- this.el.dom.className = this.parseAppURI(this.data.uri).component
- this.el.dom.innerHTML = this.dom.html(this)
- this.data = reactify( this.dom.el, this.el )
- this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
- return tasks
- },
-
- addCSS: () => {
- if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){
- document.head.innerHTML += ``
- }
- return tasks
- },
-
- scaleDOMvsXR: () => {
- if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
- return tasks
- },
-
- addModalFunctions: () => {
- this.el.close = () => {
- this.el.dom.remove()
- this.el.removeAttribute("html")
- }
- this.el.toggleFold = () => {
- this.el.dom.querySelector(".modal").classList.toggle('fold')
- this.el.dom.querySelector('.top .fold').innerText = this.el.dom.querySelector('.modal').className.match(/fold/) ? '▢' : '_'
- }
- return tasks
- },
- }
-
- tasks
- .generateUniqueId()
- .ensureOverlay()
- .addCSS()
- .createReactiveDOMElement()
- .scaleDOMvsXR()
- .addModalFunctions()
- // finally lets add the bad boy to the DOM
- if( this.dom && this.dom.modal ){
- document.querySelector('[windowmanager]').components['windowmanager'].add( this.parseAppURI(this.data.uri).component, this.el.dom )
- }else tasks.overlay.appendChild(this.el.dom)
- }
- }
-}( AFRAME.AComponent.prototype.updateProperties)
-
-
-//
-// base CSS for XRSH apps
-//
-// limitations / some guidelines for html-mesh compatibility:
-// * no icon libraries (favicon e.g.)
-// * 'border-radius: 2px 3px 4px 5px' (applies 2px to all corners)
-//
-
-document.head.innerHTML += `
-
-`
diff --git a/com/app.js b/com/app.js
index 43a04b0..2b71f55 100644
--- a/com/app.js
+++ b/com/app.js
@@ -1,5 +1,22 @@
// this is a highlevel way of loading buildless 'apps' (a collection of js components)
+AFRAME.required = {}
+AFRAME.app = new Proxy({
+
+ add(component, entity){
+ this[component] = this[component] || []
+ this[component].push(entity)
+ },
+ foreach(cb){
+ for( let i in this ){
+ if( typeof this[i] != 'function' ) this[i].map( (app) => cb({app,component:i}) )
+ }
+ }
+},{
+ get(me,k) { return me[k] },
+ set(me,k,v){ me[k] = v }
+})
+
AFRAME.registerComponent('app', {
schema:{
"uri":{ type:"string"}
@@ -8,22 +25,25 @@ AFRAME.registerComponent('app', {
events:{
"app:ready": function(){
let {id,component,type} = this.parseAppURI(this.data.uri)
- let entity = document.createElement("a-entity")
- this.el.setAttribute(component,this.data)
+ AFRAME.app[component].map( (app) => {
+ if( !app.el.getAttribute(component) ) app.el.setAttribute(component,app.data)
+ })
}
},
init: function() {
let {id,component,type} = this.parseAppURI(this.data.uri)
- if( AFRAME.components[component] || document.head.querySelector(`script#${id}`) ) this.el.emit('app:ready',{})
- else this.require([ this.data.uri ], 'app:ready')
+ let sel = `script#${component}`
+ if( AFRAME.app[component] || AFRAME.components[component] || document.head.querySelector(sel) ) return AFRAME.app.add(component,this)
+ AFRAME.app.add(component,this)
+ this.require([ this.data.uri ], 'app:ready')
},
parseAppURI: AFRAME.AComponent.prototype.parseAppURI = function(uri){
return {
- id: String(uri).split("/").pop(), // 'a/b/c/mycom.js' => 'mycom.js'
+ id: String(uri).split("/").pop(), // 'a/b/c/mycom.js' => 'mycom.js'
component: String(uri).split("/").pop().split(".js").shift(), // 'mycom.js' => 'mycom'
- type: String(uri).split(".").pop() // 'mycom.js' => 'js'
+ type: String(uri).split(".").pop() // 'mycom.js' => 'js'
}
},
@@ -34,6 +54,10 @@ AFRAME.registerComponent('app', {
if( !packages.map ) packages = Object.values(packages)
packages.map( (package) => {
let id = package.split("/").pop()
+ // prevent duplicate requests
+ if( AFRAME.required[id] ) return
+ AFRAME.required[id] = true
+
if( !document.head.querySelector(`script#${id}`) ){
let {id,component,type} = this.parseAppURI(package)
let p = new Promise( (resolve,reject) => {
@@ -73,8 +97,169 @@ AFRAME.registerComponent('app', {
AFRAME.AComponent.prototype.initComponent = function(initComponent){
return function(){
this.el.emit( this.attrName, this)
+ this.scene = AFRAME.scenes[0] // mount scene for convenience
return initComponent.apply(this,arguments)
}
}( AFRAME.AComponent.prototype.initComponent)
+AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
+ return function(){
+ updateProperties.apply(this,arguments)
+ if( this.dom && this.data && this.data.uri ){
+
+ tasks = {
+
+ generateUniqueId: () => {
+ this.el.uid = String(Math.random()).substr(10)
+ return tasks
+ },
+
+ ensureOverlay: () => {
+ let overlay = document.querySelector('#overlay')
+ if( !overlay ){
+ overlay = document.createElement('div')
+ overlay.id = "overlay"
+ document.body.appendChild(overlay)
+ document.querySelector("a-scene").setAttribute("webxr","overlayElement:#overlay")
+ }
+ tasks.overlay = overlay
+ return tasks
+ },
+
+ createReactiveDOMElement: () => {
+ const reactify = (el,aframe) => new Proxy(this.data,{
+ get(me,k,v) { return me[k] },
+ set(me,k,v){
+ me[k] = v
+ aframe.emit(k,{el,k,v})
+ }
+ })
+ this.el.dom = document.createElement('div')
+ this.el.dom.className = this.parseAppURI(this.data.uri).component
+ this.el.dom.innerHTML = this.dom.html(this)
+ this.data = reactify( this.dom.el, this.el )
+ this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
+ return tasks
+ },
+
+ addCSS: () => {
+ if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){
+ document.head.innerHTML += ``
+ }
+ return tasks
+ },
+
+ scaleDOMvsXR: () => {
+ if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
+ return tasks
+ },
+
+ addModalFunctions: () => {
+ this.el.close = () => {
+ this.el.dom.remove()
+ this.el.removeAttribute("html")
+ }
+ return tasks
+ }
+ }
+
+ tasks
+ .generateUniqueId()
+ .ensureOverlay()
+ .addCSS()
+ .createReactiveDOMElement()
+ .scaleDOMvsXR()
+ .addModalFunctions()
+
+ tasks.overlay.appendChild(this.el.dom)
+ this.el.emit('DOMready',{el: this.el.dom})
+ }
+ }
+}( AFRAME.AComponent.prototype.updateProperties)
+//
+// base CSS for XRSH apps
+//
+// limitations / some guidelines for html-mesh compatibility:
+// * no icon libraries (favicon e.g.)
+// * 'border-radius: 2px 3px 4px 5px' (applies 2px to all corners)
+//
+
+document.head.innerHTML += `
+
+`
+
+// draw a button so we can toggle apps between 2D / XR
+let toggle = (state) => {
+ state = state || document.body.className.match(/XR/)
+ document.body.classList[ state ? 'remove' : 'add'](['XR'])
+ AFRAME.scenes[0].emit( state ? 'apps:2D' : 'apps:XR')
+}
+document.addEventListener("DOMContentLoaded", (event) => {
+ let btn = document.createElement("button")
+ btn.id = "toggle_overlay"
+ btn.innerText = 'XRSH'
+ btn.addEventListener('click', (e) => toggle() )
+ document.body.appendChild(btn)
+
+ document.querySelector('a-scene').addEventListener('enter-vr', () => toggle(true) )
+ document.querySelector('a-scene').addEventListener('exit-vr', () => toggle(false) )
+ document.querySelector('a-scene').addEventListener('loaded', () => {
+ let VRbtn = document.querySelector('a-scene .a-enter-vr')
+ let ARbtn = document.querySelector('a-scene .a-enter-ar')
+ if( VRbtn ) document.body.appendChild(VRbtn) // move to body
+ if( ARbtn ) document.body.appendChild(ARbtn) // so they will always be visible
+ })
+})