refactored navigation (because of matrix-roomswitching) + updated docs with heuristic features

This commit is contained in:
Leon van Kammen 2024-01-24 18:11:37 +00:00
parent 78eac1c12b
commit 05f4779e0e
11 changed files with 207 additions and 128 deletions

View file

@ -11,7 +11,7 @@
</head> </head>
<body> <body>
<a-scene xr-mode-ui="XRMode: xr" renderer="colorManagement: true; highRefreshRate:true" light="defaultLightsEnabled: false"> <a-scene xr-mode-ui="XRMode: xr" renderer="colorManagement: true; highRefreshRate:true; multiviewStereo:true " light="defaultLightsEnabled: false">
<a-entity id="player" wasd-controls look-controls> <a-entity id="player" wasd-controls look-controls>
<a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity> <a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity>
<a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor"> <a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor">

File diff suppressed because one or more lines are too long

View file

@ -33,8 +33,16 @@ window.AFRAME.registerComponent('xrf', {
if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) ) if( ARbutton ) ARbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#AR' ) )
if( VRbutton ) VRbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#VR' ) ) if( VRbutton ) VRbutton.addEventListener('click', () => AFRAME.XRF.hashbus.pub( '#VR' ) )
xrf.addEventListener('navigateLoaded', () => { aScene.addEventListener('enter-vr', () => {
// sometimes AFRAME resets the user position to 0,0,0 when entering VR (not sure why)
let pos = xrf.frag.pos.last
if( pos ){ AFRAME.XRF.camera.position.set(pos.x, pos.y, pos.z) }
})
xrf.addEventListener('navigateLoaded', (opts) => {
setTimeout( () => AFRAME.fade.out(),500) setTimeout( () => AFRAME.fade.out(),500)
let isLocal = opts.url.match(/^#/)
if( isLocal ) return
// *TODO* this does not really belong here perhaps // *TODO* this does not really belong here perhaps
let blinkControls = document.querySelector('[blink-controls]') let blinkControls = document.querySelector('[blink-controls]')
@ -58,29 +66,28 @@ window.AFRAME.registerComponent('xrf', {
} }
}) })
xrf.addEventListener('href', (opts) => { xrf.addEventListener('navigateLoading', (opts) => {
if( opts.click){ let p = opts.promise()
let p = opts.promise() let url = opts.url
let url = opts.xrf.string let isLocal = url.match(/^#/)
let isLocal = url.match(/^#/) let hasPos = url.match(/pos=/)
let hasPos = url.match(/pos=/) let fastFadeMs = 200
if( isLocal && hasPos ){
if( isLocal ){
if( hasPos ){
// local teleports only // local teleports only
let fastFadeMs = 200
AFRAME.fade.in(fastFadeMs) AFRAME.fade.in(fastFadeMs)
setTimeout( () => { setTimeout( () => {
p.resolve() p.resolve()
AFRAME.fade.out(fastFadeMs)
}, fastFadeMs) }, fastFadeMs)
}else if( !isLocal ){ }
AFRAME.fade.in() }else{
setTimeout( () => { AFRAME.fade.in(fastFadeMs)
p.resolve() setTimeout( () => {
setTimeout( () => AFRAME.fade.out(), 1000 ) // allow one second to load textures e.g. p.resolve()
}, AFRAME.fade.data.fadetime ) }, AFRAME.fade.data.fadetime )
}else p.resolve()
} }
}) },{weight:-1000})
// convert href's to a-entity's so AFRAME // convert href's to a-entity's so AFRAME
// raycaster can find & execute it // raycaster can find & execute it

View file

@ -8,12 +8,12 @@ connectionsComponent = {
<input type="radio" name="tab" id="login" checked> <input type="radio" name="tab" id="login" checked>
<label for="login">login</label> <label for="login">login</label>
<input type="radio" name="tab" id="networks">
<label for="networks">networks</label>
<input type="radio" name="tab" id="io"> <input type="radio" name="tab" id="io">
<label for="io">devices</label> <label for="io">devices</label>
<input type="radio" name="tab" id="networks">
<label for="networks">advanced</label>
<div class="tab"> <div class="tab">
<div id="settings"></div> <div id="settings"></div>
<table> <table>
@ -26,31 +26,6 @@ connectionsComponent = {
</table> </table>
</div> </div>
<div class="tab">
<div id="networking">
<table>
<tr>
<td>Webcam</td>
<td>
<select id="webcam"></select>
</td>
</tr>
<tr>
<td>Chat</td>
<td>
<select id="chatnetwork"></select>
</td>
</tr>
<tr>
<td>World sync</td>
<td>
<select id="scene"></select>
</td>
</tr>
</table>
</div>
</div>
<div class="tab"> <div class="tab">
<div id="devices"> <div id="devices">
<a class="badge ruler">Webcam and/or Audio</a> <a class="badge ruler">Webcam and/or Audio</a>
@ -76,6 +51,32 @@ connectionsComponent = {
</table> </table>
</div> </div>
</div> </div>
<div class="tab">
<div id="networking">
Networking a la carte:<br>
<table>
<tr>
<td>Webcam</td>
<td>
<select id="webcam"></select>
</td>
</tr>
<tr>
<td>Chat</td>
<td>
<select id="chatnetwork"></select>
</td>
</tr>
<tr>
<td>World sync</td>
<td>
<select id="scene"></select>
</td>
</tr>
</table>
</div>
</div>
</div> </div>
</div> </div>
`, `,

View file

@ -14,15 +14,17 @@
* xrf.emit('foo',123).then(...).catch(...).finally(...) * xrf.emit('foo',123).then(...).catch(...).finally(...)
*/ */
xrf.addEventListener = function(eventName, callback, scene) { xrf.addEventListener = function(eventName, callback, opts) {
if( !this._listeners ) this._listeners = [] if( !this._listeners ) this._listeners = []
callback.opts = opts || {weight: this._listeners.length}
if (!this._listeners[eventName]) { if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet // create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = []; this._listeners[eventName] = [];
} }
if( scene ) callback.scene = scene
// add the callback to the listeners array for this event name // add the callback to the listeners array for this event name
this._listeners[eventName].push(callback); this._listeners[eventName].push(callback);
// sort
this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight )
return () => { return () => {
this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback ) this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback )
} }
@ -38,9 +40,6 @@ xrf.emit = function(eventName, data){
console.groupEnd(label) console.groupEnd(label)
if( xrf.debug > 1 ) debugger if( xrf.debug > 1 ) debugger
} }
// forward to THREEjs eventbus if any
if( data.scene ) data.scene.dispatchEvent( eventName, data )
if( data.mesh ) data.mesh.dispatchEvent( eventName, data )
return xrf.emit.promise(eventName,data) return xrf.emit.promise(eventName,data)
} }

View file

@ -2,47 +2,67 @@ xrf.navigator = {}
xrf.navigator.to = (url,flags,loader,data) => { xrf.navigator.to = (url,flags,loader,data) => {
if( !url ) throw 'xrf.navigator.to(..) no url given' if( !url ) throw 'xrf.navigator.to(..) no url given'
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let hashChange = (!file && hash) || !data && xrf.model.file == file
let hasPos = String(hash).match(/pos=/)
let hashbus = xrf.hashbus let hashbus = xrf.hashbus
xrf.emit('navigate', {url,loader,data})
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) xrf
if( (!file && hash) || (!data && xrf.model.file == file) ){ // we're already loaded .emit('navigate', {url,loader,data})
if( hash == document.location.hash.substr(1) ) return // block duplicate calls .then( () => {
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
return resolve(xrf.model)
}
if( !loader ){ if( ext && !loader ){
const Loader = xrf.loaders[ext] const Loader = xrf.loaders[ext]
if( !Loader ) return reject('xrfragment: no loader passed to xrfragment for extension .'+ext) if( !Loader ) return resolve()
loader = loader || new Loader().setPath( dir ) loader = loader || new Loader().setPath( dir )
} }
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
// force relative path for files which dont include protocol or relative path if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
xrf.reset() // clear xrf objects from scene
model.file = file
// 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})
xrf.add( model.scene ) if( hashChange && !hasPos ){
xrf.navigator.updateHash(hash) hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
xrf.emit('navigateLoaded',{url,model}) xrf.navigator.updateHash(hash) // which don't require
resolve(model) return resolve(xrf.model) // positional navigation
} }
if( data ) loader.parse(data, "", onLoad ) xrf
else loader.load(url, onLoad ) .emit('navigateLoading', {url,loader,data})
.then( () => {
if( hashChange && hasPos ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url})
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
// force relative path for files which dont include protocol or relative path
if( dir ) dir = dir[0] == '.' || dir.match("://") ? dir : `.${dir}`
url = url.replace(dir,"")
loader = loader || new Loader().setPath( dir )
const onLoad = (model) => {
xrf.reset() // clear xrf objects from scene
model.file = file
// 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})
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ) loader.parse(data, "", onLoad )
else loader.load(url, onLoad )
})
})
}) })
} }
@ -50,13 +70,17 @@ xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){ window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) if( !xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion)
xrf.navigator.to( document.location.search.substr(1) + document.location.hash )
}
}) })
window.addEventListener('hashchange', function (e){ window.addEventListener('hashchange', function (e){
xrf.emit('hash', {hash: document.location.hash }) xrf.emit('hash', {hash: document.location.hash })
}) })
xrf.navigator.setupNavigateFallbacks()
// this allows selectionlines to be updated according to the camera (renderloop) // this allows selectionlines to be updated according to the camera (renderloop)
xrf.focusLine = new xrf.THREE.Group() 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.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true})
@ -69,10 +93,36 @@ xrf.navigator.init = () => {
xrf.navigator.init.inited = true xrf.navigator.init.inited = true
} }
xrf.navigator.setupNavigateFallbacks = () => {
xrf.addEventListener('navigate', (opts) => {
let {url} = opts
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
// handle http links
if( url.match(/^http/) && !xrf.loaders[ext] ){
let inIframe
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
return inIframe ? window.parent.postMessage({ url }, '*') : window.open( url, '_blank')
// in case you're running in an iframe, then use this in the parent page:
//
// window.addEventListener("message", (e) => {
// if (e.data && e.data.url){
// window.open( e.data.url, '_blank')
// }
// },
// false,
// );
}
})
}
xrf.navigator.updateHash = (hash,opts) => { xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
xrf.navigator.updateHash.active = true // important to prevent recursion
document.location.hash = hash document.location.hash = hash
xrf.navigator.updateHash.active = false
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {

View file

@ -2,6 +2,7 @@ xrf.addEventListener('dynamicKey', (opts) => {
let {scene,id,match,v} = opts let {scene,id,match,v} = opts
if( !scene ) return if( !scene ) return
let remove = [] let remove = []
// erase previous lines // erase previous lines
xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) ) xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) )
xrf.focusLine.points = [] xrf.focusLine.points = []

View file

@ -3,13 +3,11 @@ xrf.frag.defaultPredefinedViews = (opts) => {
scene.traverse( (n) => { scene.traverse( (n) => {
if( n.userData && n.userData['#'] ){ if( n.userData && n.userData['#'] ){
let frag = xrf.URI.parse( n.userData['#'] ) let frag = xrf.URI.parse( n.userData['#'] )
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate main scene XR fragments and update URL
}else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
}
} }
}) })
} }
// clicking href url with predefined view
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
xrf.hashbus.pub( opts.xrf.string )
})

View file

@ -39,26 +39,20 @@ xrf.frag.href = function(v, opts){
xrf xrf
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree .emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => { .then( () => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(v.string)
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined const isLocal = v.string[0] == '#'
const hasPos = isLocal && v.string.match(/pos=/)
const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// always commit current location in case of teleport (keep a trail of last positions before we navigate) // always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) if( isLocal && !hasPos ){
xrf.navigator.to(v.string) // let's surf to HREF! xrf.hashbus.pub( v.string, xrf.model ) // publish to hashbus
.catch( (e) => { // not something we can load }else{
let inIframe //if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.updateHash(`#${lastPos}`)
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; } xrf.navigator.to(v.string) // let's surf
return inIframe ? window.parent.postMessage({ url: v.string }, '*') : window.open( v.string, '_blank') }
// in case you're running in an iframe, then use this in the parent page:
//
// window.addEventListener("message", (e) => {
// if (e.data && e.data.url){
// window.open( e.data.url, '_blank')
// }
// },
// false,
// );
})
}) })
.catch( console.error ) .catch( console.error )
} }

View file

@ -1,19 +1,23 @@
xrf.frag.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
let pos = v
// spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
if( v.x == undefined ){ if( pos.x == undefined ){
let obj = scene.getObjectByName(v.string) let obj = scene.getObjectByName(v.string)
if( !obj ) return if( !obj ) return
let pos = obj.position.clone() pos = obj.position.clone()
obj.getWorldPosition(pos) obj.getWorldPosition(pos)
camera.position.copy(pos) camera.position.copy(pos)
}else{ }else{
// spec: direct coordinate: https://xrfragment.org/#navigating%203D // spec: direct coordinate: https://xrfragment.org/#navigating%203D
camera.position.x = v.x camera.position.x = pos.x
camera.position.y = v.y camera.position.y = pos.y
camera.position.z = v.z camera.position.z = pos.z
} }
xrf.frag.pos.last = pos // remember
camera.updateMatrixWorld() camera.updateMatrixWorld()
} }

View file

@ -19,7 +19,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let {mesh} = opts let {mesh} = opts
let scene = model.scene let scene = model.scene
scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene
if( mesh.material && !mesh.userData.src ) mesh.material.visible = false // hide placeholder object if( mesh.material && mesh.userData.src ) mesh.material.visible = false // hide placeholder object
//enableSourcePortation(scene) //enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){ if( xrf.frag.src.renderAsPortal(mesh) ){
// only add remote objects, because // only add remote objects, because