diff --git a/src/3rd/js/XRWG.js b/src/3rd/js/XRWG.js new file mode 100644 index 0000000..d6b6eaf --- /dev/null +++ b/src/3rd/js/XRWG.js @@ -0,0 +1,68 @@ +// the XRWG (XR WordGraph)is mentioned in the spec +// +// it collects metadata-keys ('foo' e.g.), names and tags across 3D scene-nodes (.userData.foo e.g.) + +let XRWG = xrf.XRWG = [] + +XRWG.word = (key) => XRWG.find( (w) => w.word == word ) + +XRWG.cleankey = (word) => String(word).replace(/[^0-9\.a-zA-Z_]/g,'') + .toLowerCase() + .replace(/.*:\/\//,'') +XRWG.get = (v,k) => XRWG.find( (x) => x[ k || 'word'] == v ) + +XRWG.match = (str,types,level) => { + level = level || 1000 + types = types || [] + let res = XRWG.filter( (n) => { + types.map( (type) => n[type] ? n = false : false ) + return n + }) + str = str.toLowerCase() + if( level <10 ) res = res.filter( (n) => n.key == str ) + if( level <20 ) res = res.filter( (n) => n.word == str || n.key == str ) + if( level <30 ) res = res.filter( (n) => n.word.match(str) || n.key == str ) + if( level <40 ) res = res.filter( (n) => n.word.match(str) || n.key == str || String(n.value||'').match(str) ) + if( level <1001 ) res = res.filter( (n) => n.word.match(str) != null || n.key.match(str) != null || String(n.value||'').match(str) != null) + return res +} + +XRWG.generate = (opts) => { + let {scene,model} = opts + XRWG.slice(0,0) // empty + + // collect words from 3d nodes + + let add = (key, spatialNode, type) => { + if( !key || key.match(/(^#$|name)/) ) return + let node = XRWG.get( XRWG.cleankey(key) ) + if( node ){ + node.nodes.push(spatialNode) + }else{ + node = { word: XRWG.cleankey(key), key: key.toLowerCase(), nodes:[spatialNode] } + if( spatialNode.userData[key] ) node.value = spatialNode.userData[key] + node[type] = true + xrf.emit('XRWG',node) + XRWG.push( node ) + } + } + + scene.traverse( (o) => { + add( `#${o.name}`, o, 'name') + for( let k in o.userData ){ + if( k == 'tag' ){ + let tagArr = o.userData.tag.split(" ") + .map( (t) => t.trim() ) + .filter( (t) => t ) + .map( (w) => add( w, o, 'tag') ) + }else if( k.match(/^(href|src)$/) ) add( o.userData[k], o, k) + else if( k[0] == '#' ) add( k, o , 'pv') + else add( k, o , 'query') + } + }) + + // sort by n + XRWG.sort( (a,b) => a.nodes.length - b.nodes.length ) + XRWG = XRWG.reverse() // the cleankey/get functions e.g. will persist + console.dir(XRWG) +} diff --git a/src/3rd/js/aframe/index.js b/src/3rd/js/aframe/index.js index 7e4067f..133e287 100644 --- a/src/3rd/js/aframe/index.js +++ b/src/3rd/js/aframe/index.js @@ -2,81 +2,86 @@ window.AFRAME.registerComponent('xrf', { schema: { }, init: function () { - if( !AFRAME.XRF ) this.initXRFragments() + if( !AFRAME.XRF ){ + document.querySelector('a-scene').addEventListener('loaded', () => { + + //window.addEventListener('popstate', clear ) + //window.addEventListener('pushstate', clear ) + + // enable XR fragments + let aScene = document.querySelector('a-scene') + let XRF = AFRAME.XRF = xrf.init({ + THREE, + camera: aScene.camera, + scene: aScene.object3D, + renderer: aScene.renderer, + debug: true, + loaders: { + gltf: THREE.GLTFLoader, // which 3D assets (exts) to check for XR fragments? + glb: THREE.GLTFLoader + } + }) + if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare ABOVE entities with xrf-attributes' + + // override the camera-related XR Fragments so the camera-rig is affected + let camOverride = (xrf,v,opts) => { + opts.camera = document.querySelector('[camera]').object3D.parent + xrf(v,opts) + } + + xrf.pos = camOverride + + // in order to set the rotation programmatically + // we need to disable look-controls + xrf.rot = (xrf,v,opts) => { + let {frag,renderer} = opts; + if( frag.q ) return // camera was not targeted for rotation + let look = document.querySelector('[look-controls]') + if( look ) look.removeAttribute("look-controls") + // camOverride(xrf,v,opts) + // *TODO* make look-controls compatible, because simply + // adding the look-controls will revert to the old rotation (cached somehow?) + //setTimeout( () => look.setAttribute("look-controls",""), 100 ) + } + + // convert portal to a-entity so AFRAME + // raycaster can find & execute it + xrf.href = (xrf,v,opts) => { + camOverride(xrf,v,opts) + let {mesh,camera} = opts; + let el = document.createElement("a-entity") + el.setAttribute("xrf-get",mesh.name ) + el.setAttribute("class","ray") + el.addEventListener("click", mesh.userData.XRF.href.exec ) + $('a-scene').appendChild(el) + } + + // cleanup xrf-get objects when resetting scene + xrf.reset = ((reset) => () => { + reset() + console.log("aframe reset") + let els = [...document.querySelectorAll('[xrf-get]')] + els.map( (el) => document.querySelector('a-scene').removeChild(el) ) + })(XRF.reset) + + // undo lookup-control shenanigans (which blocks updating camerarig position in VR) + aScene.addEventListener('enter-vr', () => document.querySelector('[camera]').object3D.parent.matrixAutoUpdate = true ) + + AFRAME.XRF.navigator.to(this.data) + .then( (model) => { + let gets = [ ...document.querySelectorAll('[xrf-get]') ] + gets.map( (g) => g.emit('update') ) + }) + + aScene.emit('XRF',{}) + }) + } + if( typeof this.data == "string" ){ if( document.location.search || document.location.hash.length > 1 ){ // override url this.data = `${document.location.search.substr(1)}${document.location.hash}` } - AFRAME.XRF.navigator.to(this.data) - .then( (model) => { - let gets = [ ...document.querySelectorAll('[xrf-get]') ] - gets.map( (g) => g.emit('update') ) - }) } }, - initXRFragments: function(){ - - //window.addEventListener('popstate', clear ) - //window.addEventListener('pushstate', clear ) - - // enable XR fragments - let aScene = document.querySelector('a-scene') - let XRF = AFRAME.XRF = xrf.init({ - THREE, - camera: aScene.camera, - scene: aScene.object3D, - renderer: aScene.renderer, - debug: true, - loaders: { - gltf: THREE.GLTFLoader, // which 3D assets (exts) to check for XR fragments? - glb: THREE.GLTFLoader - } - }) - if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare ABOVE entities with xrf-attributes' - - // override the camera-related XR Fragments so the camera-rig is affected - let camOverride = (xrf,v,opts) => { - opts.camera = document.querySelector('[camera]').object3D.parent - xrf(v,opts) - } - - xrf.pos = camOverride - - // in order to set the rotation programmatically - // we need to disable look-controls - xrf.rot = (xrf,v,opts) => { - let {frag,renderer} = opts; - if( frag.q ) return // camera was not targeted for rotation - let look = document.querySelector('[look-controls]') - if( look ) look.removeAttribute("look-controls") - camOverride(xrf,v,opts) - // *TODO* make look-controls compatible, because simply - // adding the look-controls will revert to the old rotation (cached somehow?) - //setTimeout( () => look.setAttribute("look-controls",""), 100 ) - } - - // convert portal to a-entity so AFRAME - // raycaster can find & execute it - xrf.href = (xrf,v,opts) => { - camOverride(xrf,v,opts) - let {mesh,camera} = opts; - let el = document.createElement("a-entity") - el.setAttribute("xrf-get",mesh.name ) - el.setAttribute("class","ray") - el.addEventListener("click", mesh.userData.XRF.href.exec ) - $('a-scene').appendChild(el) - } - - // cleanup xrf-get objects when resetting scene - xrf.reset = ((reset) => () => { - reset() - console.log("aframe reset") - let els = [...document.querySelectorAll('[xrf-get]')] - els.map( (el) => document.querySelector('a-scene').removeChild(el) ) - })(XRF.reset) - - // undo lookup-control shenanigans (which blocks updating camerarig position in VR) - aScene.addEventListener('enter-vr', () => document.querySelector('[camera]').object3D.parent.matrixAutoUpdate = true ) - }, }) diff --git a/src/3rd/js/index.js b/src/3rd/js/index.js index ba0b117..c2ec1aa 100644 --- a/src/3rd/js/index.js +++ b/src/3rd/js/index.js @@ -29,6 +29,7 @@ xrf.roundrobin = (frag, store) => { return store.rr[label].index = 0 } +xrf.hasTag = (tag,tags) => String(tags).match( new RegExp(`(^| )${tag}( |$)`,`g`) ) // map library functions to xrf for ( let i in xrfragment ) xrf[i] = xrfragment[i] diff --git a/src/3rd/js/pubsub.js b/src/3rd/js/pubsub.js index 35415a3..1aced63 100644 --- a/src/3rd/js/pubsub.js +++ b/src/3rd/js/pubsub.js @@ -47,6 +47,8 @@ xrf.emit.promise = function(e, opts){ return { resolve, reject } } xrf.emit.normal(e, opts) + delete opts.XRF if( !opts.promise.halted ) resolve() + delete opts.promise }) } diff --git a/src/3rd/js/three/hashbus.js b/src/3rd/js/three/hashbus.js new file mode 100644 index 0000000..e6e696c --- /dev/null +++ b/src/3rd/js/three/hashbus.js @@ -0,0 +1,46 @@ +// the hashbus (QueryString eventBus) is mentioned in the spec +// +// it allows metadata-keys ('foo' e.g.) of 3D scene-nodes (.userData.foo e.g.) to +// react by executing code + +let pub = function( url, model, flags ){ // evaluate fragments in url + if( !url ) return + if( !url.match(/#/) ) url = `#${url}` + model = model || xrf.model + let { THREE, camera } = xrf + let frag = xrf.URI.parse( url, flags != undefined ? flags : xrf.XRF.NAVIGATOR ) + let opts = {frag, mesh:xrf.camera, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE, hashbus: xrf.hashbus } + xrf.emit('hashbus',opts) + .then( () => { + for ( let k in frag ){ + pub.fragment(k,opts) + } + }) + return frag +} + +pub.mesh = (mesh,model) => { // evaluate embedded fragments (metadata) inside mesh of model + if( mesh.userData ){ + let frag = {} + for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag ) + for( let k in frag ){ + let opts = {frag, mesh, model, camera: xrf.camera, scene: model.scene, renderer: xrf.renderer, THREE: xrf.THREE, hashbus: xrf.hashbus } + mesh.userData.XRF = frag // allow fragment impl to access XRF obj already + xrf.emit('mesh',opts) + .then( () => pub.fragment(k,opts) ) + } + } +} + +pub.fragment = (k, opts ) => { // evaluate one fragment + let frag = opts.frag[k]; + // call native function (xrf/env.js e.g.), or pass it to user decorator + xrf.emit(k,opts) + .then( () => { + let func = xrf.frag[k] || function(){} + if( xrf[k] ) xrf[k]( func, frag, opts) + else func( frag, opts) + }) +} + +xrf.hashbus = { pub } diff --git a/src/3rd/js/three/index.js b/src/3rd/js/three/index.js index 468d526..1669506 100644 --- a/src/3rd/js/three/index.js +++ b/src/3rd/js/three/index.js @@ -43,56 +43,26 @@ xrf.parseModel = function(model,url){ let file = xrf.getFile(url) model.file = file // eval embedded XR fragments - model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) ) + model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) ) // add animations model.clock = new xrf.THREE.Clock(); model.mixer = new xrf.THREE.AnimationMixer(model.scene) model.animations.map( (anim) => model.mixer.clipAction( anim ).play() ) + + let tmp = new xrf.THREE.Vector3() model.render = function(){ model.mixer.update( model.clock.getDelta() ) - xrf.navigator.material.selection.color.r = (1.0 + Math.sin( model.clock.getElapsedTime() * 10 ))/2 + + // update focusline + xrf.focusLine.material.color.r = (1.0 + Math.sin( model.clock.getElapsedTime() ))/2 + xrf.focusLine.material.dashSize = 0.2 + 0.02*Math.sin( model.clock.getElapsedTime() ) + xrf.focusLine.material.gapSize = 0.1 + 0.02*Math.sin( model.clock.getElapsedTime() *3 ) + xrf.focusLine.material.opacity = 0.25 + 0.15*Math.sin( model.clock.getElapsedTime() * 3 ) } } xrf.getLastModel = () => xrf.model.last -xrf.eval = function( url, model, flags ){ // evaluate fragments in url - if( !url ) return - if( !url.match(/#/) ) url = `#${url}` - model = model || xrf.model - let { THREE, camera } = xrf - let frag = xrf.URI.parse( url, flags != undefined ? flags : xrf.XRF.NAVIGATOR ) - let opts = {frag, mesh:xrf.camera, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE } - xrf.emit('eval',opts) - .then( () => { - for ( let k in frag ){ - xrf.eval.fragment(k,opts) - } - }) - return frag -} - -xrf.eval.mesh = (mesh,model) => { // evaluate embedded fragments (metadata) inside mesh of model - if( mesh.userData ){ - let frag = {} - for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag ) - for( let k in frag ){ - let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE } - mesh.userData.XRF = frag // allow fragment impl to access XRF obj already - xrf.emit('eval',opts) - .then( () => xrf.eval.fragment(k,opts) ) - } - } -} - -xrf.eval.fragment = (k, opts ) => { // evaluate one fragment - let frag = opts.frag[k]; - // call native function (xrf/env.js e.g.), or pass it to user decorator - let func = xrf.frag[k] || function(){} - if( xrf[k] ) xrf[k]( func, frag, opts) - else func( frag, opts) -} - xrf.reset = () => { const disposeObject = (obj) => { if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child)); diff --git a/src/3rd/js/three/navigator.js b/src/3rd/js/three/navigator.js index 85f2474..08c85cc 100644 --- a/src/3rd/js/three/navigator.js +++ b/src/3rd/js/three/navigator.js @@ -3,11 +3,13 @@ xrf.navigator = {} xrf.navigator.to = (url,flags,loader,data) => { if( !url ) throw 'xrf.navigator.to(..) no url given' + let hashbus = xrf.hashbus + return new Promise( (resolve,reject) => { let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) if( !file || xrf.model.file == file ){ // we're already loaded - xrf.eval( url, xrf.model, flags ) // and eval local URI XR fragments + hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments xrf.navigator.updateHash(hash) return resolve(xrf.model) } @@ -29,14 +31,13 @@ xrf.navigator.to = (url,flags,loader,data) => { // only change url when loading *another* file if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash ) xrf.model = model + // spec: 1. generate the XRWG + xrf.XRWG.generate({model,scene:model.scene}) // spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view) xrf.frag.defaultPredefinedView({model,scene:model.scene}) // spec: 2. execute predefined view(s) from URL (https://xrfragment.org/#predefined_view) - xrf.eval( url, model ) // and eval URI XR fragments + hashbus.pub( url, model ) // and eval URI XR fragments xrf.add( model.scene ) - if( !hash.match(/pos=/) ){ - xrf.eval( '#pos=0,0,0' ) // set default position if not specified - } xrf.navigator.updateHash(hash) resolve(model) } @@ -51,21 +52,28 @@ xrf.navigator.init = () => { window.addEventListener('popstate', function (event){ xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) }) - xrf.navigator.material = { - selection: new xrf.THREE.LineBasicMaterial({color:0xFF00FF,linewidth:2}) - } + + // this allows selectionlines to be updated according to the camera (renderloop) + xrf.focusLine = new xrf.THREE.Group() + xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true}) + xrf.focusLine.isXRF = true + xrf.focusLine.position.set(0,0,-0.5); + xrf.focusLine.points = [] + xrf.focusLine.lines = [] + xrf.camera.add(xrf.focusLine) + xrf.navigator.init.inited = true } -xrf.navigator.updateHash = (hash) => { - if( hash == document.location.hash || hash.match(/\|/) ) return // skip unnecesary pushState triggers +xrf.navigator.updateHash = (hash,opts) => { + if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers console.log(`URL: ${document.location.search.substr(1)}#${hash}`) document.location.hash = hash - xrf.emit('updateHash', {hash} ) + xrf.emit('hash', {...opts, hash: `#${hash}` }) } xrf.navigator.pushState = (file,hash) => { if( file == document.location.search.substr(1) ) return // page is in its default state - console.log("pushstate") window.history.pushState({},`${file}#${hash}`, document.location.pathname + `?${file}#${hash}` ) + xrf.emit('pushState', {file, hash} ) } diff --git a/src/3rd/js/three/xrf/href.js b/src/3rd/js/three/xrf/href.js index 4a17011..5aae76f 100644 --- a/src/3rd/js/three/xrf/href.js +++ b/src/3rd/js/three/xrf/href.js @@ -91,7 +91,7 @@ xrf.frag.href = function(v, opts){ let click = mesh.userData.XRF.href.exec = (e) => { let isLocal = v.string[0] == '#' - let lastPos = `pos=${camera.position.x.toFixed(1)},${camera.position.y.toFixed(1)},${camera.position.z.toFixed(1)}` + let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}` xrf .emit('href',{click:true,mesh,xrf:v}) // let all listeners agree .then( () => { @@ -100,7 +100,7 @@ xrf.frag.href = function(v, opts){ if( !v.string.match(/pos=/) ) v.string += `${v.string[0] == '#' ? '&' : '#'}${lastPos}` if( !document.location.hash.match(/pos=/) ) xrf.navigator.to(`#${lastPos}`,flags) - xrf.navigator.to(v.string,flags) // let's surf to HREF! + xrf.navigator.to(v.string) // let's surf to HREF! }) } diff --git a/src/3rd/js/three/xrf/predefinedView.js b/src/3rd/js/three/xrf/predefinedView.js index 5731213..2a28b42 100644 --- a/src/3rd/js/three/xrf/predefinedView.js +++ b/src/3rd/js/three/xrf/predefinedView.js @@ -13,53 +13,78 @@ xrf.frag.updatePredefinedView = (opts) => { let id = frag.string let oldSelection if(!id) return id // important: ignore empty strings - if( mesh.selection ) oldSelection = mesh.selection // Selection of Interest if predefined_view matches object name - if( mesh.visible && (id == mesh.name || id.substr(1) == mesh.userData.class) ){ - xrf.emit('selection',{...opts,frag}) + if( mesh.visible ){ + xrf.emit('focus',{...opts,frag}) .then( () => { - const margin = 1.2 - mesh.scale.multiplyScalar( margin ) - mesh.selection = new xrf.THREE.BoxHelper(mesh,0xff00ff) - mesh.scale.divideScalar( margin ) - mesh.selection.material.dispose() - mesh.selection.material = xrf.navigator.material.selection - mesh.selection.isXRF = true - scene.add(mesh.selection) - }) - } - return oldSelection - } + const color = new THREE.Color(); + const colors = [] + let from = new THREE.Vector3() - // spec: https://xrfragment.org/#predefined_view - const predefinedView = (frag,scene,mesh) => { - let id = frag.string || frag.fragment - id = `#${id}` - if( id == '##' ) id = '#'; // default predefined view - if( !id ) return // prevent empty matches - if( mesh.userData[id] ){ // get alias - frag = xrf.URI.parse( mesh.userData[id], xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) - xrf.emit('predefinedView',{...opts,frag}) - .then( () => { - for ( let k in frag ){ - let opts = {frag, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE } - if( frag[k].is( xrf.XRF.PV_EXECUTE ) && scene.XRF_PV_ORIGIN != k ){ // cyclic detection - traverseScene(frag[k],scene) // recurse predefined views - } - } + let getCenterPoint = (mesh) => { + var geometry = mesh.geometry; + geometry.computeBoundingBox(); + var center = new THREE.Vector3(); + geometry.boundingBox.getCenter( center ); + mesh.localToWorld( center ); + return center; + } + + xrf.camera.updateMatrixWorld(true); // always keeps me diving into the docs :] + xrf.camera.getWorldPosition(from) + from.y -= 0.5 // originate from the heart chakra! :p + const points = [from, getCenterPoint(mesh) ] + const geometry = new THREE.BufferGeometry().setFromPoints( points ); + let line = new THREE.Line( geometry, xrf.focusLine.material ); + line.isXRF = true + line.computeLineDistances(); + xrf.focusLine.lines.push(line) + xrf.focusLine.points.push(from) + scene.add(line) }) } } - const traverseScene = (v,scene) => { - let remove = [] + //// spec: https://xrfragment.org/#predefined_view + //const predefinedView = (frag,scene,mesh) => { + // let id = frag.string || frag.fragment + // id = `#${id}` + // if( id == '##' ) id = '#'; // default predefined view + // if( !id ) return // prevent empty matches + // if( mesh.userData[id] ){ // get alias + // frag = xrf.URI.parse( mesh.userData[id], xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) + // xrf.emit('predefinedView',{...opts,frag}) + // .then( () => { + // for ( let k in frag ){ + // let opts = {frag, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE } + // if( frag[k].is( xrf.XRF.PV_EXECUTE ) && scene.XRF_PV_ORIGIN != k ){ // cyclic detection + // highlightInScene(frag[k],scene) // recurse predefined views + // } + // } + // }) + // } + //} + + const highlightInScene = (v,scene) => { if( !scene ) return - scene.traverse( (mesh) => { - remove.push( selectionOfInterest( v, scene, mesh ) ) - predefinedView( v , scene, mesh ) - }) - remove.filter( (e) => e ).map( (selection) => { - scene.remove(selection) + let remove = [] + let id = v.string || v.fragment + if( id == '#' ) return + let match = xrf.XRWG.match(id) + console.dir({id,match,XRWG:xrf.XRWG}) + // erase previous lines + xrf.focusLine.lines.map( (line) => scene.remove(line) ) + xrf.focusLine.points = [] + xrf.focusLine.lines = [] + + scene.traverse( (n) => n.selection ? remove.push(n) : false ) + remove.map( (n) => scene.remove(n.selection) ) + // create new selections + match.map( (w) => { + w.nodes.map( (mesh) => { + if( mesh.material ) + selectionOfInterest( v, scene, mesh ) + }) }) } @@ -74,15 +99,16 @@ xrf.frag.updatePredefinedView = (opts) => { if( v.is( xrf.XRF.PV_EXECUTE ) ){ scene.XRF_PV_ORIGIN = v.string // wait for nested instances to arrive at the scene ? - traverseScene(v,scene) + highlightInScene(v,scene) } } } } -// react to url changes -xrf.addEventListener('updateHash', (opts) => { +// react to enduser typing url +xrf.addEventListener('hash', (opts) => { let frag = xrf.URI.parse( opts.hash, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) + console.dir({opts,frag}) xrf.frag.updatePredefinedView({frag,scene:xrf.scene}) }) @@ -92,10 +118,3 @@ xrf.addEventListener('href', (opts) => { let frag = xrf.URI.parse( opts.xrf.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) xrf.frag.updatePredefinedView({frag,scene:xrf.scene,href:opts.xrf}) }) - -//let updateUrl = (opts) => { -// console.dir(opts) -//} -// -//xrf.addEventListener('predefinedView', updateUrl ) -//xrf.addEventListener('selection', updateUrl ) diff --git a/src/3rd/js/three/xrf/q.js b/src/3rd/js/three/xrf/q.js index a911ae7..279a4dd 100644 --- a/src/3rd/js/three/xrf/q.js +++ b/src/3rd/js/three/xrf/q.js @@ -11,7 +11,7 @@ xrf.frag.q = function(v, opts){ scene.traverse( (o) => { for ( let name in v.query ) { let qobj = v.query[name]; - if( qobj.class && o.userData.class && o.userData.class == name ) objs.push(o) + if( qobj.tag && o.userData.tag && xrf.hasTag(name,o.userData.tag) ) objs.push(o) else if( qobj.id && o.name == name ) objs.push(o) } }) @@ -26,16 +26,15 @@ xrf.frag.q = function(v, opts){ xrf.frag.q.filter = function(scene,frag){ // spec: https://xrfragment.org/#queries - let q = frag.q.query + let q = frag.q.query scene.traverse( (mesh) => { for ( let i in q ) { let isMeshId = q[i].id != undefined - let isMeshClass = q[i].class != undefined - let isMeshProperty = q[i].rules != undefined && q[i].rules.length && !isMeshId && !isMeshClass + let isMeshProperty = q[i].rules != undefined && q[i].rules.length && !isMeshId if( q[i].root && mesh.isSRC ) continue; // ignore nested object for root-items (queryseletor '/foo' e.g.) - if( isMeshId && i == mesh.name ) mesh.visible = q[i].id - if( isMeshClass && i == mesh.userData.class ) mesh.visible = q[i].class - if( isMeshProperty && mesh.userData[i] ) mesh.visible = (new xrf.Query(frag.q.string)).testProperty(i,mesh.userData[i]) + if( isMeshId && + (i == mesh.name || xrf.hasTag(i,mesh.userData.tag))) mesh.visible = q[i].id + if( isMeshProperty && mesh.userData[i] ) mesh.visible = (new xrf.Query(frag.q.string)).testProperty(i,mesh.userData[i]) } }) } diff --git a/src/3rd/js/three/xrf/src.js b/src/3rd/js/three/xrf/src.js index 82223db..f743669 100644 --- a/src/3rd/js/three/xrf/src.js +++ b/src/3rd/js/three/xrf/src.js @@ -3,7 +3,7 @@ xrf.frag.src = function(v, opts){ opts.embedded = v // indicate embedded XR fragment - let { mesh, model, camera, scene, renderer, THREE} = opts + let { mesh, model, camera, scene, renderer, THREE, hashbus} = opts console.log(" └ instancing src") let src = new THREE.Group() @@ -15,24 +15,26 @@ xrf.frag.src = function(v, opts){ // cherrypicking of object(s) if( !frag.q ){ for( var i in frag ){ - if( scene.getObjectByName(i) ) src.add( obj = scene.getObjectByName(i).clone() ) - xrf.eval.fragment(i, Object.assign(opts,{frag, model,scene})) + if( scene.getObjectByName(i) ) src.add( obj = scene.getObjectByName(i).clone(true) ) + hashbus.pub.fragment(i, Object.assign(opts,{frag, model,scene})) } if( src.children.length == 1 ) obj.position.set(0,0,0); } // filtering of objects using query if( frag.q ){ - src = scene.clone(); - src.isSRC = true; + src = scene.clone(true); + src.isSRC = src.isXRF = true; xrf.frag.q.filter(src,frag) } src.traverse( (m) => { - m.isSRC = true + src.isSRC = src.isXRF = true; if( m.userData && (m.userData.src || m.userData.href) ) return ; // prevent infinite recursion - xrf.eval.mesh(m,{scene,recursive:true}) // cool idea: recursion-depth based distance between face & src + hashbus.pub.mesh(m,{scene,recursive:true}) // cool idea: recursion-depth based distance between face & src }) xrf.frag.src.scale( src, opts ) + xrf.frag.src.eval( src, opts ) + mesh.add( src ) } const externalSRC = () => { @@ -52,24 +54,23 @@ xrf.frag.src = function(v, opts){ else externalSRC() // external file } +xrf.frag.src.eval = function(scene, opts, url){ + let { mesh, model, camera, renderer, THREE, hashbus} = opts + if( url ){ + let frag = xrfragment.URI.parse(url) + // scale URI XR Fragments (queries) inside src-value + for( var i in frag ){ + hashbus.pub.fragment(i, Object.assign(opts,{frag, model:{scene},scene})) + } + hashbus.pub( '#', {scene} ) // execute the default projection '#' (if exist) + hashbus.pub( url, {scene} ) // and eval URI XR fragments + } +} + // scale embedded XR fragments https://xrfragment.org/#scaling%20of%20instanced%20objects xrf.frag.src.scale = function(scene, opts, url){ let { mesh, model, camera, renderer, THREE} = opts let restrictToBoundingBox = mesh.geometry - if( url ){ - let frag = xrfragment.URI.parse(url) - console.log("parse url:"+url) - console.log("children:"+scene.children.length) - // scale URI XR Fragments (queries) inside src-value - for( var i in frag ){ - xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene},scene})) - } - //if( frag.q ) scene = frag.q.scene - //xrf.add( model.scene ) - xrf.eval( '#', {scene} ) // execute the default projection '#' (if exist) - xrf.eval( url, {scene} ) // and eval URI XR fragments - //if( !hash.match(/pos=/) ) // xrf.eval( '#pos=0,0,0' ) // set default position if not specified - } if( restrictToBoundingBox ){ // spec 3 of https://xrfragment.org/#src // spec 1 of https://xrfragment.org/#scaling%20of%20instanced%20objects @@ -86,7 +87,6 @@ xrf.frag.src.scale = function(scene, opts, url){ scene.scale.multiply( mesh.scale ) } scene.isXRF = model.scene.isSRC = true - mesh.add( scene ) if( !opts.recursive && mesh.material ) mesh.material.visible = false // lets hide the preview object because deleting disables animations+nested objs } @@ -112,6 +112,7 @@ xrf.frag.src.type['unknown'] = function( url, opts ){ xrf.frag.src.type['model/gltf+json'] = function( url, opts ){ return new Promise( (resolve,reject) => { + let {mesh} = opts let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) let loader @@ -124,6 +125,8 @@ xrf.frag.src.type['model/gltf+json'] = function( url, opts ){ const onLoad = (model) => { xrf.frag.src.scale( model.scene, {...opts, model, scene: model.scene}, url ) + xrf.frag.src.eval( model.scene, {...opts, model, scene: model.scene}, url ) + mesh.add( model.scene ) resolve(model) } diff --git a/src/3rd/js/three/xrmacro/bg.js b/src/3rd/js/three/xrmacro/bg.js index 365e2d1..37bbc09 100644 --- a/src/3rd/js/three/xrmacro/bg.js +++ b/src/3rd/js/three/xrmacro/bg.js @@ -1,8 +1,6 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('bg', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts - if( frag.bg ){ - console.log("└ bg "+v.x+","+v.y+","+v.z); - if( scene.background ) delete scene.background - scene.background = new THREE.Color( v.x, v.y, v.z ) - } + console.log("└ bg "+v.x+","+v.y+","+v.z); + if( scene.background ) delete scene.background + scene.background = new THREE.Color( v.x, v.y, v.z ) }) diff --git a/src/3rd/js/three/xrmacro/env.js b/src/3rd/js/three/xrmacro/env.js index 4040665..7bb77bf 100644 --- a/src/3rd/js/three/xrmacro/env.js +++ b/src/3rd/js/three/xrmacro/env.js @@ -1,8 +1,9 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('env', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( frag.env && !scene.environment ){ - let env = mesh.getObjectByName(frag.env.string) - if( !env ) return console.warn("xrf.env "+v.string+" not found") + let env = scene.getObjectByName(frag.env.string) + if( !env ) env = xrf.scene.getObjectByName(frag.env.string) // repurpose from parent scene + if( !env ) return console.warn("xrf.env "+frag.env.string+" not found") env.material.map.mapping = THREE.EquirectangularReflectionMapping; scene.environment = env.material.map //scene.texture = env.material.map @@ -10,4 +11,5 @@ xrf.addEventListener('eval', (opts) => { renderer.toneMappingExposure = 2; console.log(` └ applied image '${frag.env.string}' as environment map`) } + }) diff --git a/src/3rd/js/three/xrmacro/fog.js b/src/3rd/js/three/xrmacro/fog.js index b19ea94..2d03aa5 100644 --- a/src/3rd/js/three/xrmacro/fog.js +++ b/src/3rd/js/three/xrmacro/fog.js @@ -1,11 +1,9 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('fog', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts - if( frag.fog ){ - let v = frag.fog - console.log("└ fog "+v.x+","+v.y); - if( v.x == 0 && v.y == 0 ){ - if( scene.fog ) delete scene.fog - scene.fog = null; - }else scene.fog = new THREE.Fog( scene.background, v.x, v.y ); - } + let v = frag.fog + console.log("└ fog "+v.x+","+v.y); + if( v.x == 0 && v.y == 0 ){ + if( scene.fog ) delete scene.fog + scene.fog = null; + }else scene.fog = new THREE.Fog( scene.background, v.x, v.y ); }) diff --git a/src/3rd/js/three/xrmacro/macro.js b/src/3rd/js/three/xrmacro/macro.js index c3d3db0..6f9b6b3 100644 --- a/src/3rd/js/three/xrmacro/macro.js +++ b/src/3rd/js/three/xrmacro/macro.js @@ -1,7 +1,8 @@ xrf.macros = {} -xrf.addEventListener('eval', (opts) => { - let { frag, mesh, model, camera, scene, renderer, THREE} = opts +xrf.addEventListener('mesh', (opts) => { + let { frag, mesh, model, camera, scene, renderer, THREE, hashbus} = opts + for( let k in frag ){ let id = mesh.name+"_"+k let fragment = frag[k] @@ -25,8 +26,7 @@ xrf.addEventListener('eval', (opts) => { if( xrf.macros[ rrFrag ] ){ xrf.macros[ rrFrag ].trigger() } else { - if( rrFrag[0] == '#' ) xrf.navigator.updateHash(rrFrag) - else xrf.eval(rrFrag,null,0) + xrf.navigator.to( rrFrag,null,0) } }) } diff --git a/src/3rd/js/three/xrmacro/mov.js b/src/3rd/js/three/xrmacro/mov.js index 24038b0..90431e4 100644 --- a/src/3rd/js/three/xrmacro/mov.js +++ b/src/3rd/js/three/xrmacro/mov.js @@ -1,4 +1,4 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('mov', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( frag.mov && frag.q ){ diff --git a/src/3rd/js/three/xrmacro/pos.js b/src/3rd/js/three/xrmacro/pos.js index 0b76eb5..074153d 100644 --- a/src/3rd/js/three/xrmacro/pos.js +++ b/src/3rd/js/three/xrmacro/pos.js @@ -1,4 +1,4 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('pos', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( frag.pos && frag.q ){ // apply roundrobin (if any) diff --git a/src/3rd/js/three/xrmacro/rot.js b/src/3rd/js/three/xrmacro/rot.js index 3f61590..efb3e96 100644 --- a/src/3rd/js/three/xrmacro/rot.js +++ b/src/3rd/js/three/xrmacro/rot.js @@ -1,4 +1,4 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('rot', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( frag.rot && frag.q ){ // apply roundrobin (if any) diff --git a/src/3rd/js/three/xrmacro/scale.js b/src/3rd/js/three/xrmacro/scale.js index 9b66940..35fb241 100644 --- a/src/3rd/js/three/xrmacro/scale.js +++ b/src/3rd/js/three/xrmacro/scale.js @@ -1,4 +1,4 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('scale', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( frag.scale && frag.q ){ // apply roundrobin (if any) diff --git a/src/3rd/js/three/xrmacro/show.js b/src/3rd/js/three/xrmacro/show.js index 85f7d14..8aa23f3 100644 --- a/src/3rd/js/three/xrmacro/show.js +++ b/src/3rd/js/three/xrmacro/show.js @@ -1,4 +1,4 @@ -xrf.addEventListener('eval', (opts) => { +xrf.addEventListener('show', (opts) => { let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( frag.show && frag.q ){ let show = frag.show diff --git a/src/spec/query.selectors.json b/src/spec/query.selectors.json index 15bfe77..c24f462 100644 --- a/src/spec/query.selectors.json +++ b/src/spec/query.selectors.json @@ -1,16 +1,15 @@ [ - {"fn":"query","data":"class:bar", "expect":{ "fn":"testProperty","input":["class","bar"],"out":true}}, - {"fn":"query","data":".bar", "expect":{ "fn":"testProperty","input":["class","bar"],"out":true}, "label":".bar shorthand"}, - {"fn":"query","data":".bar -.foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":false}}, - {"fn":"query","data":".bar -.foo .foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":true}}, - {"fn":"query","data":".bar -.bar .bar", "expect":{ "fn":"testProperty","input":["class","bar"],"out":true}}, - {"fn":"query","data":".foo -.foo .foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":true},"label":"class:foo"}, - {"fn":"query","data":".foo -.foo bar:5 .foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":true},"label":"class:foo"}, - {"fn":"query","data":".foo -.foo bar:>5 .foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":true},"label":"class:foo"}, - {"fn":"query","data":".foo -.foo bar:>5 .foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":true},"label":"class:foo"}, - {"fn":"query","data":".foo -.foo .foo", "expect":{ "fn":"testProperty","input":["class","foo"],"out":true},"label":"class:foo"}, - {"fn":"query","data":".foo -.foo .foo", "expect":{ "fn":"testProperty","input":["id","foo"],"out":false},"label":"!id:foo"}, - {"fn":"query","data":"foo -foo foo", "expect":{ "fn":"testProperty","input":["id","foo"],"out":true},"label":"id:foo?"}, + {"fn":"query","data":"tag:bar", "expect":{ "fn":"testProperty","input":["tag","bar"],"out":true}}, + {"fn":"query","data":"tag:bar -tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":false}}, + {"fn":"query","data":"tag:bar -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true}}, + {"fn":"query","data":"tag:bar -tag:bar tag:bar", "expect":{ "fn":"testProperty","input":["tag","bar"],"out":true}}, + {"fn":"query","data":"tag:foo -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"}, + {"fn":"query","data":"tag:foo -tag:foo bar:5 tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"}, + {"fn":"query","data":"tag:foo -tag:foo bar:>5 tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"}, + {"fn":"query","data":"tag:foo -tag:foo bar:>5 tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"}, + {"fn":"query","data":"tag:foo -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"}, + {"fn":"query","data":"tag:foo -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["id","foo"],"out":true},"label":"id:foo"}, + {"fn":"query","data":"tag:foo -foo foo", "expect":{ "fn":"testProperty","input":["id","foo"],"out":true},"label":"id:foo?"}, {"fn":"query","data":"/foo", "expect":{ "fn":"testQueryRoot","input":["foo"],"out":true},"label":"foo should be root-only"}, {"fn":"query","data":"/foo foo", "expect":{ "fn":"testQueryRoot","input":["foo"],"out":false},"label":"foo should recursively selected"} ] diff --git a/src/spec/url.json b/src/spec/url.json index 72ef8c1..0944b6d 100644 --- a/src/spec/url.json +++ b/src/spec/url.json @@ -13,5 +13,5 @@ {"fn":"url","data":"http://foo.com?foo=1#mypredefinedview&another", "expect":{ "fn":"testPredefinedView", "input":"mypredefinedview","out":true},"label":"test predefined view executed (multiple)"}, {"fn":"url","data":"#cube.position.x=music.position.x", "expect":{ "fn":"testPropertyAssign", "input":"cube.position.x","out":true},"label":"test data assign"}, {"fn":"url","data":"#cube.position.x=@music.position.x", "expect":{ "fn":"testPropertyAssign", "input":"cube.position.x","out":true},"label":"test one-way data bind"}, - {"fn":"url","data":"http://foo.com?foo=1#mycustom=foo", "expect":{ "fn":"testParsed", "input":"_mycustom","out":true},"label":"test custom property"} + {"fn":"url","data":"http://foo.com?foo=1#mycustom=foo", "expect":{ "fn":"testParsed", "input":"mycustom","out":true},"label":"test custom property"} ] diff --git a/src/xrfragment/Parser.hx b/src/xrfragment/Parser.hx index 5d652fb..eb0e128 100644 --- a/src/xrfragment/Parser.hx +++ b/src/xrfragment/Parser.hx @@ -21,7 +21,7 @@ class Parser { // category: href navigation / portals / teleporting Frag.set("href", XRF.ASSET | XRF.T_URL | XRF.T_PREDEFINED_VIEW ); - Frag.set("class", XRF.ASSET | XRF.T_STRING ); + Frag.set("tag", XRF.ASSET | XRF.T_STRING ); // category: query selector / object manipulation Frag.set("pos", XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 | XRF.T_STRING_OBJ | XRF.METADATA | XRF.NAVIGATOR ); diff --git a/src/xrfragment/Query.hx b/src/xrfragment/Query.hx index d66c42b..735a858 100644 --- a/src/xrfragment/Query.hx +++ b/src/xrfragment/Query.hx @@ -51,11 +51,10 @@ class Query { // 1. requirement: receive arguments: query (string) private var str:String = ""; - private var q:haxe.DynamicAccess = {}; // 1. create an associative array/object to store query-arguments as objects + private var q:haxe.DynamicAccess = {}; // 1. create an associative array/object to store query-arguments as objects private var isProp:EReg = ~/^.*:[><=!]?/; // 1. detect object id's & properties `foo:1` and `foo` (reference regex: `/^.*:[><=!]?/` ) private var isExclude:EReg = ~/^-/; // 1. detect excluders like `-foo`,`-foo:1`,`-.foo`,`-/foo` (reference regex: `/^-/` ) private var isRoot:EReg = ~/^[-]?\//; // 1. detect root selectors like `/foo` (reference regex: `/^[-]?\//` ) - private var isClass:EReg = ~/^[-]?class$/; // 1. detect class selectors like `.foo` (reference regex: `/^[-]?class$/` ) private var isNumber:EReg = ~/^[0-9\.]+$/; // 1. detect number values like `foo:1` (reference regex: `/^[0-9\.]+$/` ) public function new(str:String){ @@ -66,12 +65,6 @@ class Query { return this.q; } - public function expandAliases(token:String) : String { - // expand '.foo' to 'class:foo' - var classAlias = ~/^(-)?\./; - return classAlias.match(token) ? StringTools.replace(token,".","class:") : token; // 1. expand aliases like `.foo` into `class:foo` - } - public function get() : Dynamic { return this.q; } @@ -102,16 +95,11 @@ class Query { k = k.substr(1); // 1. then strip key-operator: convert "-foo" into "foo" }else v = v.substr(oper.length); // 1. then strip value operator: change value ">=foo" into "foo" if( oper.length == 0 ) oper = "="; - if( isClass.match(k) ){ - filter[ prefix+ k ] = oper != "!="; - q.set(v,filter); - }else{ - var rule:haxe.DynamicAccess = {}; - if( isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v); - else rule[oper] = v; - filter['rules'].push( rule ); // 1. add operator and value to rule-array - q.set( k, filter ); - } + var rule:haxe.DynamicAccess = {}; + if( isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v); + else rule[oper] = v; + filter['rules'].push( rule ); // 1. add operator and value to rule-array + q.set( k, filter ); return; }else{ // 1. ELSE we are dealing with an object filter[ "id" ] = isExclude.match(str) ? false: true; // 1. therefore we we set `id` to `true` or `false` (false=excluder `-`) @@ -121,7 +109,7 @@ class Query { q.set( str ,filter ); // 1. finally we add the key/value to the store (`store.foo = {id:false,root:true}` e.g.) } } - for( i in 0...token.length ) process( expandAliases(token[i]) ); + for( i in 0...token.length ) process( token[i] ); return this.q = q; }