diff --git a/com/app.js b/com/app.js index 3383b6d..4127e10 100644 --- a/com/app.js +++ b/com/app.js @@ -148,7 +148,7 @@ AFRAME.AComponent.prototype.initComponent = function(initComponent){ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){ - return function(){ + let setupApp = function(){ updateProperties.apply(this,arguments) if( !this.data || !this.data.uri || this.isApp ) return // only deal with apps (once) @@ -179,7 +179,7 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){ createReactiveDOMElement: () => { 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.el.dom.innerHTML = this.com.dom.html(this) this.el.dom.style.display = 'none' this.data = reactify( this.dom.el, this.el ) this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) ) @@ -257,6 +257,9 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){ this.isApp = true this.el.app = this } + return function(){ + try{ setupApp.apply(this,arguments) }catch(e){ console.error(e) } + } }( AFRAME.AComponent.prototype.updateProperties) document.head.innerHTML += ` diff --git a/com/control/hand-menu-button.js b/com/control/hand-menu-button.js deleted file mode 100644 index 80e56c4..0000000 --- a/com/control/hand-menu-button.js +++ /dev/null @@ -1,153 +0,0 @@ -/* global AFRAME, THREE */ -AFRAME.registerComponent('hand-menu-button', { - schema: { - src: {default: ''}, - srcHover: {default: ''}, - mixin: {default: ''} - }, - - init: function () { - this.onWatchButtonHovered = this.onWatchButtonHovered.bind(this); - this.onAnimationComplete = this.onAnimationComplete.bind(this); - this.onCollisionStarted = this.onCollisionStarted.bind(this); - this.onCollisionEnded = this.onCollisionEnded.bind(this); - this.onAnimationBegin = this.onAnimationBegin.bind(this); - this.onPinchEnded = this.onPinchEnded.bind(this); - - this.el.addEventListener('obbcollisionstarted', this.onCollisionStarted); - this.el.addEventListener('obbcollisionended', this.onCollisionEnded); - this.el.object3D.renderOrder = 1000; - - this.menuEl = this.el.parentEl; - this.handMenuEl = this.el.sceneEl.querySelector('[hand-menu]'); - - this.menuEl.addEventListener('animationbegin', this.onAnimationBegin); - this.menuEl.addEventListener('animationcomplete', this.onAnimationComplete); - }, - - onAnimationBegin: function (evt) { - // To prevent menu activations while animation is running. - if (evt.detail.name === 'animation__open') { this.menuOpen = false; } - }, - - onAnimationComplete: function (evt) { - if (evt.detail.name === 'animation__open') { this.menuOpen = true; } - if (evt.detail.name === 'animation__close') { this.menuOpen = false; } - }, - - onCollisionStarted: function (evt) { - var withEl = evt.detail.withEl; - if (this.handMenuEl === withEl || - !withEl.components['hand-tracking-controls']) { return; } - if (!this.menuOpen) { return; } - this.handHoveringEl = withEl; - this.el.emit('watchbuttonhoverstarted'); - }, - - onCollisionEnded: function (evt) { - var withEl = evt.detail.withEl; - if (this.handMenuEl === withEl || - !withEl.components['hand-tracking-controls']) { return; } - this.disableHover(); - this.handHoveringEl = undefined; - this.el.emit('watchbuttonhoverended'); - }, - - enableHover: function () { - this.handHoveringEl.addEventListener('pinchended', this.onPinchEnded); - this.el.setAttribute('material', 'src', this.data.srcHover); - }, - - disableHover: function () { - if (!this.handHoveringEl) { return; } - this.handHoveringEl.removeEventListener('pinchended', this.onPinchEnded); - this.el.setAttribute('material', 'src', this.data.src); - }, - - onPinchEnded: (function () { - var spawnPosition = new THREE.Vector3(0, 1, 0); - return function () { - var cubeEl; - var newEntityEl; - if (!this.menuOpen) { return; } - this.menuOpen = false; - if (!this.handHoveringEl || !this.data.mixin) { return; } - // Spawn shape a little above the menu. - spawnPosition.set(0, 1, 0); - // Apply rotation of the menu. - spawnPosition.applyQuaternion(this.el.parentEl.object3D.quaternion); - // 20cm above the menu. - spawnPosition.normalize().multiplyScalar(0.2); - spawnPosition.add(this.el.parentEl.object3D.position); - - newEntityEl = document.createElement('a-entity'); - newEntityEl.setAttribute('mixin', this.data.mixin); - newEntityEl.setAttribute('position', spawnPosition); - this.el.sceneEl.appendChild(newEntityEl); - this.handHoveringEl.removeEventListener('pinchended', this.onPinchEnded); - }; - })(), - - onWatchButtonHovered: function (evt) { - if (evt.target === this.el || !this.handHoveringEl) { return; } - this.disableHover(); - this.handHoveringEl = undefined; - } -}); - -/* - User's hand can collide with multiple buttons simultaneously but only want one in a hovered state. - This system keeps track of all the collided buttons, keeping just the closest to the hand in a hovered state. -*/ -AFRAME.registerSystem('hand-menu-button', { - init: function () { - this.onWatchButtonHovered = this.onWatchButtonHovered.bind(this); - this.el.parentEl.addEventListener('watchbuttonhoverended', this.onWatchButtonHovered); - this.el.parentEl.addEventListener('watchbuttonhoverstarted', this.onWatchButtonHovered); - this.hoveredButtonEls = []; - }, - - tick: function () { - var buttonWorldPosition = new THREE.Vector3(); - var thumbPosition; - var smallestDistance = 1000000; - var currentDistance; - var closestButtonEl; - if (this.hoveredButtonEls.length < 2) { return; } - thumbPosition = this.hoveredButtonEls[0].components['hand-menu-button'].handHoveringEl.components['obb-collider'].trackedObject3D.position; - for (var i = 0; i < this.hoveredButtonEls.length; ++i) { - this.hoveredButtonEls[i].object3D.getWorldPosition(buttonWorldPosition); - currentDistance = buttonWorldPosition.distanceTo(thumbPosition); - if (currentDistance < smallestDistance) { - closestButtonEl = this.hoveredButtonEls[i]; - smallestDistance = currentDistance; - } - } - - if (this.hoveredButtonEl === closestButtonEl) { return; } - - this.hoveredButtonEl = closestButtonEl; - - for (i = 0; i < this.hoveredButtonEls.length; ++i) { - if (!this.hoveredButtonEls[i].components['hand-menu-button'].handHoveringEl) { continue; } - if (this.hoveredButtonEls[i] === closestButtonEl) { - this.hoveredButtonEls[i].components['hand-menu-button'].enableHover(); - continue; - } - this.hoveredButtonEls[i].components['hand-menu-button'].disableHover(); - } - }, - - onWatchButtonHovered: function (evt) { - this.buttonEls = this.el.sceneEl.querySelectorAll('[hand-menu-button]'); - this.hoveredButtonEls = []; - for (var i = 0; i < this.buttonEls.length; ++i) { - if (!this.buttonEls[i].components['hand-menu-button'].handHoveringEl) { continue; } - this.hoveredButtonEls.push(this.buttonEls[i]); - } - if (this.hoveredButtonEls.length === 1) { - this.hoveredButtonEl = this.hoveredButtonEls[0]; - this.hoveredButtonEls[0].components['hand-menu-button'].enableHover(); - } - } -}); diff --git a/com/control/hand-menu.js b/com/control/hand-menu.js deleted file mode 100644 index 987cd79..0000000 --- a/com/control/hand-menu.js +++ /dev/null @@ -1,198 +0,0 @@ -/* global AFRAME, THREE */ -AFRAME.registerComponent('hand-menu', { - schema: { - location: {default: 'palm', oneOf: ['palm']} - }, - - menuHTML: /* syntax: html */ ` - - - - - - - - - - - - - - - - - - `, - - init: function () { - this.onCollisionStarted = this.onCollisionStarted.bind(this); - this.onCollisionEnded = this.onCollisionEnded.bind(this); - this.onSceneLoaded = this.onSceneLoaded.bind(this); - this.onEnterVR = this.onEnterVR.bind(this); - - this.throttledOnPinchEvent = AFRAME.utils.throttle(this.throttledOnPinchEvent, 50, this); - - //this.el.sceneEl.addEventListener('loaded', this.onSceneLoaded); - this.el.sceneEl.addEventListener('enter-vr', this.onEnterVR); - }, - - onEnterVR: function () { - this.onSceneLoaded() - this.setupMenu(); - }, - - onSceneLoaded: function () { - var handEls = this.el.sceneEl.querySelectorAll('[hand-tracking-controls]'); - for (var i = 0; i < handEls.length; i++) { - if (handEls[i] === this.el) { continue; } - this.handElement = handEls[i]; - } - }, - - setupMenu: function () { - var template = document.createElement('template'); - template.innerHTML = this.menuHTML; - this.menuEl = template.content.children[0]; - this.el.sceneEl.appendChild(this.menuEl); - - if (this.data.location === 'palm') { - this.setupPalm(); - } - }, - - setupPalm: function () { - var el = this.openMenuEl = document.createElement('a-entity'); - el.setAttribute('geometry', 'primitive: circle; radius: 0.025'); - el.setAttribute('material', 'side: double; src: #palmButton; shader: flat'); - el.setAttribute('rotation', '90 0 180'); - el.setAttribute('position', '0 -0.035 -0.07'); - el.setAttribute('obb-collider', ''); - el.addEventListener('obbcollisionstarted', this.onCollisionStarted); - el.addEventListener('obbcollisionended', this.onCollisionEnded); - this.el.appendChild(el); - }, - - throttledOnPinchEvent: function (evt) { - if (evt.type === 'pinchstarted') { this.onPinchStarted(evt); } - if (evt.type === 'pinchended') { this.onPinchEnded(evt); } - }, - - onCollisionStarted: function (evt) { - var withEl = evt.detail.withEl; - if (this.handElement !== withEl) { return; } - withEl.addEventListener('pinchstarted', this.throttledOnPinchEvent); - withEl.addEventListener('pinchended', this.throttledOnPinchEvent); - this.handHoveringEl = withEl; - this.updateUI(); - }, - - onCollisionEnded: function (evt) { - var withEl = evt.detail.withEl; - if (this.handElement !== withEl) { return; } - withEl.removeEventListener('pinchstarted', this.throttledOnPinchEvent); - if (!this.opened) { - withEl.removeEventListener('pinchended', this.throttledOnPinchEvent); - } - this.handHoveringEl = undefined; - this.updateUI(); - }, - - updateUI: function () { - var palmButtonImage; - if (this.data.location === 'palm') { - palmButtonImage = this.handHoveringEl ? '#palmButtonHover' : '#palmButton'; - debugger - this.openMenuEl.setAttribute('material', 'src', palmButtonImage); - return; - } - }, - - onPinchStarted: (function () { - var auxMatrix = new THREE.Matrix4(); - var auxQuaternion = new THREE.Quaternion(); - return function (evt) { - if (!this.handHoveringEl || this.opened) { return; } - this.opened = true; - this.menuEl.object3D.position.copy(evt.detail.position); - this.menuEl.emit('open'); - function lookAtVector (sourcePoint, destPoint) { - return auxQuaternion.setFromRotationMatrix( - auxMatrix.identity() - .lookAt(sourcePoint, destPoint, new THREE.Vector3(0, 1, 0))); - } - - var cameraEl = this.el.sceneEl.querySelector('[camera]'); - var rotationQuaternion = lookAtVector(this.menuEl.object3D.position, cameraEl.object3D.position); - this.menuEl.object3D.quaternion.copy(rotationQuaternion); - this.pinchedEl = this.handHoveringEl; - if (this.data.location === 'palm') { this.openMenuEl.object3D.visible = false; } - }; - })(), - - onPinchEnded: function (evt) { - if (!this.pinchedEl) { return; } - this.opened = false; - this.menuEl.emit('close'); - this.pinchedEl = undefined; - this.openMenuEl.object3D.visible = true; - }, - - lookAtCamera: (function () { - var auxVector = new THREE.Vector3(); - var auxObject3D = new THREE.Object3D(); - return function (el) { - var cameraEl = this.el.sceneEl.querySelector('[camera]'); - auxVector.subVectors(cameraEl.object3D.position, el.object3D.position).add(el.object3D.position); - el.object3D.lookAt(auxVector); - el.object3D.rotation.z = 0; - }; - })() -}); - -/* - -Watch style UI that work both in VR and AR with @aframevr in one line of - -Try now on @Meta Quest Browser - -https://a-watch.glitch.me/ - -Just 400 lines of code: https://glitch.com/edit/#!/a-watch - -Watch-style intuitive but easy to occlude hands ⌚️ -Palm- style less familiar but more robust ✋ - -Enjoy! Wanna see more of this? sponsor me on @github - -https://github.com/sponsors/dmarcos - -*/ diff --git a/com/dom.js b/com/dom.js new file mode 100644 index 0000000..2b6cd53 --- /dev/null +++ b/com/dom.js @@ -0,0 +1,127 @@ +/* + * ## dom + * + * instances reactive DOM component from AFRAME component's `dom` metadata + * + * ```html + * + * + * + * ``` + * + * | property | type | example | + * |--------------|--------------------|----------------------------------------------------------------------------------------| + * | `com` | `array` of strings | | + * + * | event | target | info | + * |--------------|-------------------------------------------------------------------------------------------------------------| + * | `DOMready` | self | fired when dom component (`this.dom`) is created | + */ + +AFRAME.registerComponent('dom',{ + + init: function(){ + Object.values(this.el.components) + .map( (c) => { + if( c.dom && c.attrName != "dom"){ + this.dom = c.dom + this.com = c + } + }) + if( !this.dom ) return console.warn('dom.js did not find a .dom object inside components') + + this + .ensureOverlay() + .addCSS() + .createReactiveDOMElement() + .scaleDOMvsXR() + .triggerKeyboardForInputs() + .setupListeners() + + document.querySelector('#overlay').appendChild(this.el.dom) + this.el.emit('DOMready',{el: this.el.dom}) + }, + + ensureOverlay(){ + // ensure overlay + 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") + } + return this + }, + + reactify: function(el,data){ + return new Proxy(data, { + get(me,k,v) { + return me[k] + }, + set(me,k,v){ + me[k] = v + el.emit(k,{el,k,v}) + } + }) + }, + + // creates el.dom (the 2D DOM object) + createReactiveDOMElement: function(){ + this.el.dom = document.createElement('div') + 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] + return this + }, + + addCSS: function(){ + if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){ + document.head.innerHTML += `` + } + return this + }, + + scaleDOMvsXR: function(){ + if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`) + return this + }, + + setupListeners: function(){ + this.el.sceneEl.addEventListener('apps:2D', () => this.el.setAttribute('visible', false) ) + this.el.sceneEl.addEventListener('apps:XR', () => { + this.el.setAttribute('visible', true) + this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`) + }) + return this + }, + + triggerKeyboardForInputs: function(){ + // https://developer.oculus.com/documentation/web/webxr-keyboard ; + [...this.el.dom.querySelectorAll('[type=text]')].map( (input) => { + let triggerKeyboard = function(){ + this.focus() + console.log("focus") + } + input.addEventListener('click', triggerKeyboard ) + }) + return this + } + +}) diff --git a/app/example/helloworld-html.js b/com/example/helloworld-html.js similarity index 100% rename from app/example/helloworld-html.js rename to com/example/helloworld-html.js diff --git a/app/example/helloworld-htmlform.js b/com/example/helloworld-htmlform.js similarity index 100% rename from app/example/helloworld-htmlform.js rename to com/example/helloworld-htmlform.js diff --git a/app/example/helloworld-iframe.js b/com/example/helloworld-iframe.js similarity index 100% rename from app/example/helloworld-iframe.js rename to com/example/helloworld-iframe.js diff --git a/app/example/helloworld-window.js b/com/example/helloworld-window.js similarity index 78% rename from app/example/helloworld-window.js rename to com/example/helloworld-window.js index 8a103b8..2f1a353 100644 --- a/app/example/helloworld-window.js +++ b/com/example/helloworld-window.js @@ -3,12 +3,10 @@ AFRAME.registerComponent('helloworld-window', { foo: { type:"string"} }, - init: function(){}, + dependencies: ['dom'], - 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", // + init: function(){ + this.el.object3D.visible = false }, dom: { @@ -23,9 +21,6 @@ AFRAME.registerComponent('helloworld-window', { events:{ - // component events - html: function( ){ console.log("html-mesh requirement mounted") }, - // combined AFRAME+DOM reactive events click: function(e){ }, // keydown: function(e){ }, // @@ -33,14 +28,16 @@ AFRAME.registerComponent('helloworld-window', { // reactive events for this.data updates myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue }, - ready: function( ){ + launcher: async function(){ this.el.dom.style.display = 'none' - console.log("this.el.dom has been added to DOM") + await AFRAME.utils.require({ + dom: "./com/dom.js", // interpret .dom object + 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", // + }) this.data.myvalue = 1 setInterval( () => this.data.myvalue++, 100 ) - }, - - launcher: function(){ console.log("this.el.dom has been added to DOM") new WinBox("Hello World",{ width: 250, @@ -58,8 +55,8 @@ AFRAME.registerComponent('helloworld-window', { }, manifest: { // HTML5 manifest to identify app to xrsh - "short_name": "Hello world", - "name": "Hello world", + "short_name": "Hello world window", + "name": "Hello world window", "icons": [ { "src": "https://css.gg/browser.svg", diff --git a/app/example/helloworld.js b/com/example/helloworld.js similarity index 85% rename from app/example/helloworld.js rename to com/example/helloworld.js index 4c9ede9..7c0f930 100644 --- a/app/example/helloworld.js +++ b/com/example/helloworld.js @@ -1,8 +1,11 @@ AFRAME.registerComponent('helloworld', { schema: { - foo: { type:"string"} + wireframe: { type:"boolean", "default":false}, + foo: {type:"string","default":"foo"} }, + dependencies:['dom'], + init: function () { this.el.object3D.visible = false @@ -12,22 +15,15 @@ AFRAME.registerComponent('helloworld', { ` - - this.interval = setInterval( () => this.data.wireframe = !this.data.wireframe, 500 ) - }, - - requires:{ - // somecomponent: "https://unpkg.com/some-aframe-component/mycom.min.js" }, events:{ - // component events - somecomponent: function( ){ console.log("component requirement mounted") }, - ready: function(e){ console.log("requires are loaded") }, - launcher: function(e){ this.el.object3D.visible = !this.el.object3D.visible + this.interval = setInterval( () => { + this.data.wireframe = !this.data.wireframe + }, 500 ) }, // reactive this.data value demo diff --git a/app/launcher.js b/com/launcher.js similarity index 54% rename from app/launcher.js rename to com/launcher.js index 72d166c..5da96f0 100644 --- a/app/launcher.js +++ b/com/launcher.js @@ -18,30 +18,29 @@ AFRAME.registerComponent('launcher', { schema: { - foo: { type:"string"} + attach: { type:"selector"} }, - init: function () { + dependencies:['dom'], + + init: async function () { this.data.apps = [] - AFRAME.scenes.map( (scene) => { - scene.addEventListener('app:ready', (e) => this.render(e.detail) ) + await AFRAME.utils.require({ + html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME + dom: "./com/dom.js", + svgfile: "https://7dir.github.io/aframe-svgfile-component/aframe-svgfile-component.min.js", }) - }, - requires:{ - html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME - 'hand-menu': "./com/control/hand-menu.js", - 'hand-menu-button': "./com/control/hand-menu-button.js", + this.el.setAttribute("dom","") + this.render() + this.el.sceneEl.addEventListener('enter-vr', (e) => this.render() ) }, dom: { scale: 3, events: ['click'], - html: (me) => `
-
-
`, - + html: (me) => `
loading components..
`, css: (me) => `#iconmenu { z-index: 1000; display: flex; @@ -102,47 +101,67 @@ AFRAME.registerComponent('launcher', { events:{ - // combined AFRAME+DOM reactive events - click: function(e){ - console.dir(e) - }, // - - - ready: function( ){ - this.el.dom.children[0].id = this.el.uid // important hint for html-mesh - document.querySelector('#left-hand').setAttribute('hand-menu','') - }, - }, - render: function(app){ - clearTimeout(this.timeout) - this.timeout = setTimeout( () => { - const drawButton = (app) => { - if( !app.manifest ) return - console.log(app.data.uri+" "+app.data.order) - let btn = app.btn = document.createElement('button') - if( app.manifest.icons?.length > 0){ - let img = document.createElement('img') - img.src = app.manifest.icons[0].src - img.title = app.manifest.name + ": " + app.manifest.description - btn.appendChild(img) - }else btn.innerText = app.manifest.short_name - btn.addEventListener('click', () => { - app.el.emit('launcher',app) - app.el.sceneEl.emit('launched',app) - }) - this.el.dom.querySelector('#iconmenu').appendChild(btn) + render: async function(){ + if( !this.el.dom ) return // too early (dom.js component not ready) + + let inVR = this.sceneEl && this.sceneEl.renderer.xr.isPresenting + let items = [...this.el.children] + let requires = [] + let i = 0 + let colors = [ + '#4C73FE', + '#554CFE', + '#864CFE', + '#B44CFE', + '#E24CFE', + '#FE4CD3' + ] + + const add2D = (launchCom,el,manifest,aentity) => { + let btn = document.createElement('button') + btn.innerHTML = `${ manifest?.icons?.length > 0 + ? `` + : `${manifest.short_name}` + }` + btn.addEventListener('click', () => el.emit('launcher',{}) ) + this.el.dom.appendChild(btn) + } + + const add3D = (launchCom,el,manifest) => { + let aentity = document.createElement('a-entity') + let atext = document.createElement('a-entity') + aentity.setAttribute("mixin","menuitem") + aentity.setAttribute("position",`${i++ * 0.2} 0 0`) + if( !aentity.getAttribute("material")){ + aentity.setAttribute('material',`side: double; color: ${colors[ i % colors.length]}`) } + atext.setAttribute("text",`value: ${manifest.short_name}; align: baseline; anchor: align; align:center; wrapCount:7`) + atext.setAttribute("scale","0.1 0.1 0.1") + atext.setAttribute("position","0 0 0.0") + aentity.appendChild(atext) + this.el.appendChild(aentity) + return aentity + } - const drawCategory = (app,c) => app.manifest && - app.manifest.short_name != "launcher" && - app.manifest.category == c ? drawButton(app) : false + // finally render them! + this.el.dom.innerHTML = '' // clear + this.system.components.map( (c) => { + const launchComponentKey = c.getAttributeNames().pop() + const launchCom = c.components[ launchComponentKey ] + if( !launchCom ) return console.warn(`could not find component '${launchComponentKey}' (forgot to include script-tag?)`) + const manifest = launchCom.manifest + if( manifest ){ + add2D(launchCom,c,manifest, add3D(launchCom,c,manifest) ) + } + }) - AFRAME.app.foreach( (app) => drawCategory(app,undefined) ) - AFRAME.app.foreach( (app) => drawCategory(app,"system") ) + if( this.data.attach ){ + this.el.object3D.visible = inVR ? true : false + // if( inVR ) this.data.attach.appendChild(this.el) + } - },100) }, manifest: { // HTML5 manifest to identify app to xrsh @@ -199,3 +218,37 @@ in above's case "\nHelloworld application\n" will qualify as header. }); + +AFRAME.registerSystem('launcher',{ + + init: function(){ + this.components = [] + // observer HTML changes in + observer = new MutationObserver( (a,b) => this.getLaunchables(a,b) ) + observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false}); + }, + + getLaunchables: function(mutationsList,observer){ + let searchEvent = 'launcher' + let els = [...this.sceneEl.getElementsByTagName("*")] + + this.components = 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] ){ + hasEvent = true + } + } + } + return hasEvent ? el : null + }) + this.updateLauncher() + }, + + updateLauncher: function(){ + let launcher = document.querySelector('[launcher]') + if( launcher ) launcher.components['launcher'].render() + } + +}) diff --git a/com/osbutton.js b/com/osbutton.js deleted file mode 100644 index 32b6440..0000000 --- a/com/osbutton.js +++ /dev/null @@ -1,86 +0,0 @@ -AFRAME.registerComponent('osbutton',{ - - data:{ - width: {type: 'number', default: 0.4}, - height: {type: 'number', default: 0.2}, - depth: {type: 'number', default: 0.06}, - color: {type: 'color', default: 'blue'}, - distance: {type: 'number'}, - label: {type: 'string'} - }, - - init: function(){ - this - .createBox() - .setupDistanceTick() - }, - - setupDistanceTick: function(){ - // we throttle by distance, to support scenes with loads of clickable objects (far away) - if( !this.data.distance ) this.data.distance = 0.9 - this.distance = -1 - this.worldPosition = new THREE.Vector3() - this.posCam = new THREE.Vector3() - this.tick = this.throttleByDistance( () => this.showSource() ) - }, - - createBox: function(){ - let geometry = this.geometry = new THREE.BoxGeometry(this.data.width, this.data.height, this.data.depth); - this.material = new THREE.MeshStandardMaterial({color: this.data.color }); - this.mesh = new THREE.Mesh(this.geometry, this.material); - this.scaleChildToButton(this.el.object3D, this.mesh) - this.el.object3D.add(this.mesh) - return this - }, - - throttleByDistance: function(f){ - return function(){ - if( this.distance < 0 ) return f() // first call - if( !f.tid ){ - let x = this.distance - let y = x*(x*0.05)*1000 // parabolic curve - f.tid = setTimeout( function(){ - f.tid = null - f() - }, y ) - } - } - }, - - showSource: function(){ - this.el.sceneEl.camera.getWorldPosition(this.posCam) - this.el.object3D.getWorldPosition(this.worldPosition) - this.distance = this.posCam.distanceTo(this.worldPosition) - - if( this.distance < this.data.distance ){ - this.material.side = THREE.BackSide - }else{ - this.material.side = THREE.FrontSide - } - }, - - scaleChildToButton: function(scene, mesh ){ - let cleanScene = scene.clone() - let remove = [] - const notVisible = (n) => !n.visible || (n.material && !n.material.visible) - cleanScene.traverse( (n) => notVisible(n) && n.children.length == 0 && (remove.push(n)) ) - remove.map( (n) => n.removeFromParent() ) - let restrictTo3DBoundingBox = mesh.geometry - if( restrictTo3DBoundingBox ){ - // normalize instanced objectsize to boundingbox - let sizeFrom = new THREE.Vector3() - let sizeTo = new THREE.Vector3() - let empty = new THREE.Object3D() - new THREE.Box3().setFromObject(mesh).getSize(sizeTo) - new THREE.Box3().setFromObject(cleanScene).getSize(sizeFrom) - let ratio = sizeFrom.divide(sizeTo) - scene.children.map( (c) => { - if( c.uuid != mesh.uuid ){ - c.scale.multiplyScalar( 1.0 / Math.max(ratio.x, ratio.y, ratio.z)) - } - }) - } - - }, - -}) diff --git a/app/paste.js b/com/paste.js similarity index 100% rename from app/paste.js rename to com/paste.js diff --git a/app/save.js b/com/save.js similarity index 92% rename from app/save.js rename to com/save.js index 5067f68..445fcca 100644 --- a/app/save.js +++ b/com/save.js @@ -9,21 +9,12 @@ AFRAME.registerComponent('save', { //this.el.innerHTML = ` ` }, - requires:{ - // somecomponent: "https://unpkg.com/some-aframe-component/mycom.min.js" - }, - events:{ - // component events - somecomponent: function( ){ console.log("component requirement mounted") }, - ready: function(e){ console.log("requires are loaded") }, - launcher: function(e){ this.save() }, - }, save: function(){ diff --git a/app/xrfragments.js b/com/xrfragments.js similarity index 68% rename from app/xrfragments.js rename to com/xrfragments.js index 2aae5ce..733a934 100644 --- a/app/xrfragments.js +++ b/com/xrfragments.js @@ -3,30 +3,33 @@ AFRAME.registerComponent('xrfragments', { url: { type:"string"} }, - init: function () { - }, + dependencies:['dom'], - requires:{ - xrfragments: "https://xrfragment.org/dist/xrfragment.aframe.js", + init: function () { }, events:{ - // requires are loaded - ready: function(e){ - this.el.setAttribute("xrf","https://coderofsalvation.github.io/xrsh-media/assets/background.glb") - - let ARbutton = document.querySelector('.a-enter-ar-button') - if( ARbutton ){ - ARbutton.addEventListener('click', () => { - AFRAME.XRF.reset() - }) - } - }, - - launcher: function(){ + launcher: async function(){ let url = prompt('enter URL to glb/fbx/json/obj/usdz asset', 'https://xrfragment.org/index.glb') - if( url ) AFRAME.XRF.navigator.to(url) + if( !url ) return + await AFRAME.utils.require({ + xrfragments: "https://xrfragment.org/dist/xrfragment.aframe.js", + }) + + // remove objects which are marked to be removed from scene (with noxrf) + let els = [...document.querySelectorAll('[noxrf]') ] + els.map( (el) => el.remove() ) + + if( !this.el.getAttribute("xrf") ){ + this.el.setAttribute("xrf", url ) + let ARbutton = document.querySelector('.a-enter-ar-button') + if( ARbutton ){ + ARbutton.addEventListener('click', () => { + AFRAME.XRF.reset() + }) + } + }else AFRAME.XRF.navigator.to(url) } },