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

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( 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)
let isLocal = opts.url.match(/^#/)
if( isLocal ) return
// *TODO* this does not really belong here perhaps
let blinkControls = document.querySelector('[blink-controls]')
@ -58,29 +66,28 @@ window.AFRAME.registerComponent('xrf', {
}
})
xrf.addEventListener('href', (opts) => {
if( opts.click){
let p = opts.promise()
let url = opts.xrf.string
let isLocal = url.match(/^#/)
let hasPos = url.match(/pos=/)
if( isLocal && hasPos ){
xrf.addEventListener('navigateLoading', (opts) => {
let p = opts.promise()
let url = opts.url
let isLocal = url.match(/^#/)
let hasPos = url.match(/pos=/)
let fastFadeMs = 200
if( isLocal ){
if( hasPos ){
// local teleports only
let fastFadeMs = 200
AFRAME.fade.in(fastFadeMs)
setTimeout( () => {
p.resolve()
AFRAME.fade.out(fastFadeMs)
}, fastFadeMs)
}else if( !isLocal ){
AFRAME.fade.in()
setTimeout( () => {
p.resolve()
setTimeout( () => AFRAME.fade.out(), 1000 ) // allow one second to load textures e.g.
}, AFRAME.fade.data.fadetime )
}else p.resolve()
}
}else{
AFRAME.fade.in(fastFadeMs)
setTimeout( () => {
p.resolve()
}, AFRAME.fade.data.fadetime )
}
})
},{weight:-1000})
// convert href's to a-entity's so AFRAME
// raycaster can find & execute it

View file

@ -8,12 +8,12 @@ connectionsComponent = {
<input type="radio" name="tab" id="login" checked>
<label for="login">login</label>
<input type="radio" name="tab" id="networks">
<label for="networks">networks</label>
<input type="radio" name="tab" id="io">
<label for="io">devices</label>
<input type="radio" name="tab" id="networks">
<label for="networks">advanced</label>
<div class="tab">
<div id="settings"></div>
<table>
@ -26,31 +26,6 @@ connectionsComponent = {
</table>
</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 id="devices">
<a class="badge ruler">Webcam and/or Audio</a>
@ -76,6 +51,32 @@ connectionsComponent = {
</table>
</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>
`,

View file

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

View file

@ -2,47 +2,67 @@ xrf.navigator = {}
xrf.navigator.to = (url,flags,loader,data) => {
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
xrf.emit('navigate', {url,loader,data})
return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
if( (!file && hash) || (!data && xrf.model.file == file) ){ // we're already loaded
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash)
return resolve(xrf.model)
}
xrf
.emit('navigate', {url,loader,data})
.then( () => {
if( !loader ){
const Loader = xrf.loaders[ext]
if( !Loader ) return reject('xrfragment: no loader passed to xrfragment for extension .'+ext)
loader = loader || new Loader().setPath( dir )
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
if( ext && !loader ){
const Loader = xrf.loaders[ext]
if( !Loader ) return resolve()
loader = loader || new Loader().setPath( dir )
}
// 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})
if( !hash && !file && !ext ) return resolve(xrf.model) // nothing we can do here
xrf.add( model.scene )
xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( hashChange && !hasPos ){
hashbus.pub( url, xrf.model, flags ) // eval local URI XR fragments
xrf.navigator.updateHash(hash) // which don't require
return resolve(xrf.model) // positional navigation
}
if( data ) loader.parse(data, "", onLoad )
else loader.load(url, onLoad )
xrf
.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
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){
xrf.emit('hash', {hash: document.location.hash })
})
xrf.navigator.setupNavigateFallbacks()
// 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})
@ -69,10 +93,36 @@ xrf.navigator.init = () => {
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) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
xrf.navigator.updateHash.active = true // important to prevent recursion
document.location.hash = hash
xrf.navigator.updateHash.active = false
}
xrf.navigator.pushState = (file,hash) => {

View file

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

View file

@ -3,13 +3,11 @@ xrf.frag.defaultPredefinedViews = (opts) => {
scene.traverse( (n) => {
if( n.userData && 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
.emit('href',{click:true,mesh,xrf:v}) // let all listeners agree
.then( () => {
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 )
// 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}`)
xrf.navigator.to(v.string) // let's surf to HREF!
.catch( (e) => { // not something we can load
let inIframe
try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; }
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,
// );
})
if( isLocal && !hasPos ){
xrf.hashbus.pub( v.string, xrf.model ) // publish to hashbus
}else{
//if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.updateHash(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf
}
})
.catch( console.error )
}

View file

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

View file

@ -19,7 +19,8 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let {mesh} = opts
let scene = model.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)
if( xrf.frag.src.renderAsPortal(mesh) ){
// only add remote objects, because