refactored spec, added focusLine, hashbus & XRWG
This commit is contained in:
parent
89e5af7d28
commit
0d3959359b
24 changed files with 367 additions and 261 deletions
68
src/3rd/js/XRWG.js
Normal file
68
src/3rd/js/XRWG.js
Normal 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)
|
||||
}
|
||||
|
|
@ -2,20 +2,8 @@ window.AFRAME.registerComponent('xrf', {
|
|||
schema: {
|
||||
},
|
||||
init: function () {
|
||||
if( !AFRAME.XRF ) this.initXRFragments()
|
||||
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(){
|
||||
if( !AFRAME.XRF ){
|
||||
document.querySelector('a-scene').addEventListener('loaded', () => {
|
||||
|
||||
//window.addEventListener('popstate', clear )
|
||||
//window.addEventListener('pushstate', clear )
|
||||
|
|
@ -50,7 +38,7 @@ window.AFRAME.registerComponent('xrf', {
|
|||
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)
|
||||
// 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 )
|
||||
|
|
@ -78,5 +66,22 @@ window.AFRAME.registerComponent('xrf', {
|
|||
|
||||
// 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}`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
46
src/3rd/js/three/hashbus.js
Normal file
46
src/3rd/js/three/hashbus.js
Normal 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 }
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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} )
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
let getCenterPoint = (mesh) => {
|
||||
var geometry = mesh.geometry;
|
||||
geometry.computeBoundingBox();
|
||||
var center = new THREE.Vector3();
|
||||
geometry.boundingBox.getCenter( center );
|
||||
mesh.localToWorld( center );
|
||||
return center;
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
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 )
|
||||
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 )
|
||||
})
|
||||
remove.filter( (e) => e ).map( (selection) => {
|
||||
scene.remove(selection)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -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 )
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
@ -30,11 +30,10 @@ xrf.frag.q.filter = function(scene,frag){
|
|||
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( 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])
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ){
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ class Query {
|
|||
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 );
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue