refactored spec, added focusLine, hashbus & XRWG

This commit is contained in:
Leon van Kammen 2023-09-15 19:42:37 +02:00
parent 89e5af7d28
commit 0d3959359b
24 changed files with 367 additions and 261 deletions

68
src/3rd/js/XRWG.js Normal file
View file

@ -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)
}

View file

@ -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 <a-entity camera..> 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 <a-entity camera..> 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 )
},
})

View file

@ -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]

View file

@ -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
})
}

View file

@ -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 }

View file

@ -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));

View file

@ -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} )
}

View file

@ -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!
})
}

View file

@ -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 )

View file

@ -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])
}
})
}

View file

@ -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)
}

View file

@ -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 )
})

View file

@ -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`)
}
})

View file

@ -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 );
})

View file

@ -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)
}
})
}

View file

@ -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 ){

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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"}
]

View file

@ -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"}
]

View file

@ -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 );

View file

@ -51,11 +51,10 @@ class Query {
// 1. requirement: receive arguments: query (string)
private var str:String = "";
private var q:haxe.DynamicAccess<Dynamic> = {}; // 1. create an associative array/object to store query-arguments as objects
private var q:haxe.DynamicAccess<Dynamic> = {}; // 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<Dynamic> = {};
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<Dynamic> = {};
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. <b>ELSE </b> 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;
}