update: close to release

This commit is contained in:
Leon van Kammen 2024-01-29 20:19:04 +00:00
parent ead6a93171
commit 9df7685253
21 changed files with 359139 additions and 588 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
dist/*.pyc
src/spec/tmp.json
tags
example/assets/*.blend
example/assets/*.blend*
2wa.gitlab.io/*

View File

@ -1,5 +1,5 @@
/*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024
* v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0
*/
@ -554,10 +554,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({});
var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
var $el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
@ -629,15 +629,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* 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 )
}
@ -653,9 +655,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)
}
@ -907,15 +906,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file})
}
@ -947,6 +937,8 @@ xrf.reset = () => {
// remove mixers
xrf.mixers.map( (m) => m.stop())
xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
}
xrf.parseUrl = (url) => {
@ -981,47 +973,81 @@ 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 || (!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( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
if( !loader ){
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
loader = loader || new Loader().setPath( dir )
}
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)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// 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) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,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.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
})
}
@ -1029,13 +1055,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})
@ -1048,10 +1078,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) => {
@ -1070,7 +1126,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`)
// console.log(` └ applied image '${frag.env.string}' as environment map`)
}
})
@ -1115,20 +1171,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)
//if( !file.match(/\./) || file.match(/\.html/) ){
// debugger
// 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')
//}
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 )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// 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!
//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 )
}
@ -1185,20 +1241,24 @@ xrf.frag.href = function(v, opts){
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()
}
xrf.frag.rot = function(v, opts){
@ -1242,7 +1302,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
@ -1822,16 +1883,14 @@ 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 static XR fragments
}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 )
})
xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts
let objname = v.fragment
@ -1866,6 +1925,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 = []
@ -2057,7 +2117,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null,
transparent: url.match(/(png|gif)/) ? true : false,
transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide,
color: 0xFFFFFF,
opacity:1
@ -2078,6 +2138,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
}
}
mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true
}
@ -2374,8 +2435,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]')
@ -2393,34 +2462,34 @@ window.AFRAME.registerComponent('xrf', {
el.setAttribute("class","floor")
$('a-scene').appendChild(el)
})
blinkControls.components['blink-controls'].update({collisionEntities:true})
let com = blinkControls.components['blink-controls']
if( com ) com.update({collisionEntities:true})
else console.warn("xrfragments: blink-controls is not mounted, please run manually: $('[blink-controls]).components['blink-controls'].update({collisionEntities:true})")
}
})
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 && !url.match(/^http/) ) return // dont fade/load for custom protocol handlers
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

@ -1,5 +1,5 @@
/*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024
* v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0
*/
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({});
var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
var $el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
@ -627,15 +627,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* 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 )
}
@ -651,9 +653,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)
}
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file})
}
@ -945,6 +935,8 @@ xrf.reset = () => {
// remove mixers
xrf.mixers.map( (m) => m.stop())
xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
}
xrf.parseUrl = (url) => {
@ -979,47 +971,81 @@ 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 || (!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( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
if( !loader ){
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
loader = loader || new Loader().setPath( dir )
}
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)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// 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) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,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.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
})
}
@ -1027,13 +1053,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})
@ -1046,10 +1076,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) => {
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`)
// console.log(` └ applied image '${frag.env.string}' as environment map`)
}
})
@ -1113,20 +1169,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)
//if( !file.match(/\./) || file.match(/\.html/) ){
// debugger
// 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')
//}
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 )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// 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!
//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 )
}
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
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()
}
xrf.frag.rot = function(v, opts){
@ -1240,7 +1300,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
@ -1820,16 +1881,14 @@ 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 static XR fragments
}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 )
})
xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts
let objname = v.fragment
@ -1864,6 +1923,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 = []
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null,
transparent: url.match(/(png|gif)/) ? true : false,
transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide,
color: 0xFFFFFF,
opacity:1
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
}
}
mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true
}
@ -2372,8 +2433,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]')
@ -2391,34 +2460,34 @@ window.AFRAME.registerComponent('xrf', {
el.setAttribute("class","floor")
$('a-scene').appendChild(el)
})
blinkControls.components['blink-controls'].update({collisionEntities:true})
let com = blinkControls.components['blink-controls']
if( com ) com.update({collisionEntities:true})
else console.warn("xrfragments: blink-controls is not mounted, please run manually: $('[blink-controls]).components['blink-controls'].update({collisionEntities:true})")
}
})
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 && !url.match(/^http/) ) return // dont fade/load for custom protocol handlers
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

357884
dist/xrfragment.module.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,12 @@
chatComponent = {
html: `
<div id="chat">
<div id="videos" style="pointer-events:none"></div>
<div id="messages" aria-live="assertive" aria-relevant></div>
<div id="chatfooter">
<div id="chatbar">
<input id="chatline" type="text" placeholder="type here"></input>
<input id="chatline" type="text" placeholder="chat here"></input>
</div>
<button id="showchat" class="btn">show chat</button>
</div>
@ -15,10 +16,12 @@ chatComponent = {
init: (el) => new Proxy({
scene: null,
visible: true,
visibleChatbar: false,
messages: [],
scene: null,
visible: true,
messages: [],
oneMessagePerUser: false,
username: '', // configured by 'network.connected' event
$videos: el.querySelector("#videos"),
$messages: el.querySelector("#messages"),
@ -28,16 +31,19 @@ chatComponent = {
install(opts){
this.opts = opts
this.scene = opts.scene
this.$chatbar.style.display = 'none'
el.className = "xrf"
el.style.display = 'none' // start hidden
document.body.appendChild( el )
this.visibleChatbar = false
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
},
initListeners(){
let {$chatline} = this
$chatline.addEventListener('click', (e) => this.inform() )
$chatline.addEventListener('keydown', (e) => {
if (e.key == 'Enter' ){
if( $chatline.value[0] != '/' ){
@ -45,9 +51,25 @@ chatComponent = {
}
this.send({message: $chatline.value })
$chatline.value = ''
if( window.innerHeight < 600 ) $chatline.blur()
}
})
console.dir(this.scene)
document.addEventListener('network.connect', (e) => {
this.visible = true
this.$chatbar.style.display = '' // show
})
document.addEventListener('network.connected', (e) => {
if( e.detail.username ) this.username = e.detail.username
})
},
inform(){
if( !this.inform.informed && (this.inform.informed = true) ){
window.notify("Connected via P2P. You can now type message which will be visible to others.")
}
},
toggle(){
@ -55,10 +77,20 @@ chatComponent = {
if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
},
hyphenate(str){
return String(str).replace(/[^a-zA-Z0-9]/g,'-')
},
// sending messages to the #messages div
// every user can post maximum one msg at a time
// it's more like a 'status' which is more friendly
// for accessibility reasons
// for a fullfledged chat/transcript see matrix clients
send(opts){
let {$messages} = this
opts = { linebreak:true, message:"", class:[], ...opts }
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
opts.pos = opts.pos || network.posName || network.pos
let div = document.createElement('div')
let msg = document.createElement('div')
let br = document.createElement('br')
@ -74,7 +106,12 @@ chatComponent = {
br.classList.add.apply(br.classList, opts.class)
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
}
if( !opts.from && !msg.className.match(/(info|guide)/) ) msg.classList.add('self')
if( !msg.className.match(/(info|guide|ui)/) ){
let frag = xrf.URI.parse(document.location.hash)
opts.from = 'you'
if( frag.pos ) opts.pos = frag.pos.string
msg.classList.add('self')
}
if( opts.from ){
nick.className = "user"
nick.innerText = opts.from+' '
@ -86,6 +123,15 @@ chatComponent = {
}
}
div.appendChild(msg)
// force one message per user
if( this.oneMessagePerUser && opts.from ){
div.id = this.hyphenate(opts.from)
let oldMsg = $messages.querySelector(`#${div.id}`)
if( oldMsg ) oldMsg.remove()
}
// remove after timeout
if( opts.timeout ) setTimeout( (div) => div.remove(), opts.timeout, div )
// finally add the message on top
$messages.appendChild(div)
if( opts.linebreak ) div.appendChild(br)
$messages.scrollTop = $messages.scrollHeight // scroll down
@ -110,9 +156,6 @@ chatComponent = {
if( !el.inited && (el.inited = true) ) me.initListeners()
break;
}
case "visibleChatbar": {
me.$chatbar.style.display = v ? 'block' : 'none'
}
}
}
@ -196,46 +239,63 @@ chatComponent.css = `
max-width:unset;
}
#messages{
position: absolute;
transition:1s;
top: 0px;
left: 0;
bottom: 130px;
padding: 15px;
overflow:hidden;
pointer-events:none;
transition:1s;
/*
display: flex;
flex-direction: column;
width: 91%;
max-width: 500px;
*/
width:100%;
align-items: flex-start;
position: absolute;
transition:1s;
top: 77px;
left: 0;
bottom: 49px;
padding: 20px;
overflow:hidden;
overflow-y: scroll;
pointer-events:none;
transition:1s;
z-index: 100;
-webkit-user-select:none;
-moz-user-select:-moz-none;
-ms-user-select:none;
user-select:none;
}
body.menu #messages{
top:50px;
}
#messages *{
#messages:hover {
pointer-events:all;
}
#messages *{
pointer-events:none;
-webkit-user-select:none;
-moz-user-select:-moz-none;
-ms-user-select:none;
user-select:none;
}
#messages .msg{
transition:all 1s ease;
background: #fff;
display: inline-block;
padding: 1px 17px;
border-radius: 20px 0px 20px 20px;
border-radius: 20px;
color: #000c;
margin-bottom: 10px;
line-height:23px;
pointer-events:visible;
line-height:33px;
cursor:grabbing;
border: 1px solid #0002;
}
#messages .msg *{
pointer-events:all;
-webkit-user-select:text;
-moz-user-select:-moz-text;
-ms-user-select:text;
user-select:text;
}
#messages .msg.self{
border-radius: 0px 20px 20px 20px;
background:var(--xrf-primary);
border-radius: 20px;
background:var(--xrf-box-shadow);
}
#messages .msg.self,
#messages .msg.self div{
@ -254,12 +314,16 @@ chatComponent.css = `
}
#messages .msg a {
text-decoration:underline;
color: #EEE;
color: var(--xrf-primary);
font-weight:bold;
transition:1s;
transition:0.3s;
}
#messages .msg.info a,
#messages a.ruler{
color:#FFF;
}
#messages .msg a:hover{
color:#FFF;
color:#000;
}
#messages .msg.ui,
#messages .msg.ui div{
@ -327,10 +391,19 @@ chatComponent.css = `
margin:0;
}
.envelope{
height:unset;
.envelope,
.envelope * {
overflow:hidden;
transition:1s;
pointer-events:none;
}
.envelope a,
.envelope button,
.envelope input,
.envelope textarea,
.envelope msg,
.envelope msg * {
pointer-events:all;
}
.user{
@ -346,70 +419,92 @@ connectionsComponent = {
html: `
<div id="connections">
<i class="gg-close-o" id="close" onclick="$connections.toggle()"></i>
<div id="networking">
<h2>Network channels:</h2>
<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>
<i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
<br>
<div class="tab-frame">
<input type="radio" name="tab" id="login" checked>
<label for="login">login</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>
<tr>
<td></td>
<td>
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
</td>
</tr>
</table>
</div>
<div class="tab">
<div id="devices">
<a class="badge ruler">Webcam and/or Audio</a>
<table>
<tr>
<td>Video</td>
<td>
<select id="videoInput"></select>
</td>
</tr>
<tr>
<td>Mic</td>
<td>
<select id="audioInput"></select>
</td>
</tr>
<tr style="display:none"> <!-- not used (for now) -->
<td>Audio</td>
<td>
<select id="audioOutput"></select>
</td>
</tr>
</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 id="devices">
<a class="badge ruler">Webcam</a>
<table>
<tr>
<td>Video</td>
<td>
<select id="videoInput"></select>
</td>
</tr>
<tr>
<td>Mic</td>
<td>
<select id="audioInput"></select>
</td>
</tr>
<tr style="display:none"> <!-- not used (for now) -->
<td>Audio</td>
<td>
<select id="audioOutput"></select>
</td>
</tr>
</table>
</div>
<div id="settings"></div>
<table>
<tr>
<td></td>
<td>
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
</td>
</tr>
</table>
</div>
`,
init: (el) => new Proxy({
webcam: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
chatnetwork: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
scene: [{plugin:{name:"No thanks"},config: () => document.createElement('div')}],
visible: true,
webcam: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
chatnetwork: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
scene: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
selectedWebcam: '',
selectedChatnetwork:'',
@ -435,15 +530,13 @@ connectionsComponent = {
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i>&nbsp;connect</a><br>`
]).concat($menu.buttons)
// hide networking settings if entering thru meetinglink
if( document.location.href.match(/meet=/) ) this.show()
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
},
toggle(){
let parent = el.closest('.envelope')
parent.style.display = parent.style.display == 'none' ? parent.style.display = '' : 'none'
$chat.visible = !$chat.visible
},
change(id,e){
@ -452,20 +545,28 @@ connectionsComponent = {
}
},
show(){
$chat.visible = true
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
if( !network.connected ){
if( el.parentElement ) el.parentElement.parentElement.remove()
$chat.send({message:"", el, class:['ui']})
if( !network.meetinglink ){ // set default
$webcam.value = 'Peer2Peer'
$chatnetwork.value = 'Peer2Peer'
$scene.value = 'Peer2Peer'
}
this.renderSettings()
show(opts){
opts = opts || {}
if( opts.hide ){
if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
if( !opts.showChat ) $chat.visible = false
}else{
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
$chat.visible = true
this.visible = true
// hide networking settings if entering thru meetinglink
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
if( !network.connected ){
document.querySelector('body > .xrf').appendChild(el)
$chat.send({message:"", el, class:['ui']})
if( !network.meetinglink ){ // set default
$webcam.value = opts.webcam || 'Peer2Peer'
$chatnetwork.value = opts.chatnetwork || 'Peer2Peer'
$scene.value = opts.scene || 'Peer2Peer'
}
this.renderSettings()
}else{
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
}
}
},
@ -478,7 +579,7 @@ connectionsComponent = {
forSelectedPluginsDo(cb){
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
let plugins = {}
let select = (name) => (o) => o.plugin.name == name ? plugins[ o.plugin.name ] = o : ''
let select = (name) => (o) => o.profile.name == name ? plugins[ o.profile.name ] = o : ''
this.webcam.find( select(this.selectedWebcam) )
this.chatnetwork.find( select(this.selectedChatnetwork) )
this.scene.find( select(this.selectedScene) )
@ -491,12 +592,12 @@ connectionsComponent = {
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
this.update()
$settings.innerHTML = ''
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config(opts) ) )
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
this.renderInputs()
},
renderInputs(){
if( this.selectedWebcam == 'No thanks' ){
if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
return this.$devices.style.display = 'none'
}else this.$devices.style.display = ''
@ -548,25 +649,15 @@ connectionsComponent = {
})
},
reactToNetwork(){
document.addEventListener('network.connected', () => {
console.log("network.connected")
window.notify("🪐 connected to awesomeness..")
$chat.visibleChatbar = true
$chat.send({message:`🎉 connected!`,class:['info']})
})
reactToNetwork(){ // *TODO* move to network?
document.addEventListener('network.connect', () => {
console.log("network.connect")
el.parentElement.classList.add('connecthide')
window.notify("🪐 connecting to awesomeness..")
$connect.innerText = 'connecting..'
this.show({hide:true, showChat: true})
})
document.addEventListener('network.disconnect', () => {
window.notify("🪐 disconnecting..")
$connect.innerText = 'disconnecting..'
setTimeout( () => $connect.innerText = 'connect', 1000)
if( !window.accessibility.enabled ) $chat.visibleChatbar = false
document.addEventListener('network.disconnect', () => {
this.connected = false
})
}
},{
@ -575,9 +666,10 @@ connectionsComponent = {
set(data,k,v){
data[k] = v
switch( k ){
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
case "visible": el.style.display = v ? '' : 'none'; break;
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
case "selectedScene": $scene.value = v; data.renderSettings(); break;
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
case "selectedWebcam": {
@ -620,11 +712,11 @@ connectionsComponent.css = `
}
#close{
display: block;
margin-top: 16px;
position: relative;
float: right;
margin-bottom: 7px;
top: 16px;
}
#messages .msg.ui div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
</style>`
// reactive component for displaying the menu
menuComponent = (el) => new Proxy({
@ -644,8 +736,8 @@ menuComponent = (el) => new Proxy({
$buttons: $buttons = el.querySelector('#buttons'),
$btnMore: $btnMore = el.querySelector('#more'),
toggle(){
this.collapsed = !this.collapsed
toggle(state){
this.collapsed = state !== undefined ? state : !this.collapsed
el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu'
document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu'])
},
@ -777,16 +869,19 @@ window.accessibility = (opts) => new Proxy({
document.addEventListener('network.send', (e) => {
let opts = e.detail
opts.message = opts.message || ''
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
this.speak(opts.message)
})
opts.xrf.addEventListener('pos', (opts) => {
if( this.enabled ){
$chat.send({message: this.posToMessage(opts) })
network.send({message: this.posToMessage(opts), class:["info","guide"]})
}
if( opts.frag.pos.string.match(/,/) ){
network.pos = opts.frag.pos.string
}else{
network.posName = opts.frag.pos.string
}
network.send({message: this.posToMessage(opts), class:["info","guide"]})
network.pos = opts.frag.pos.string
})
},
@ -895,6 +990,7 @@ document.head.innerHTML += `
white-space:pre;
min-width: 45px;
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
display:inline-block;
}
.xrf button:hover,
@ -997,7 +1093,7 @@ document.head.innerHTML += `
.menu .btn{
.footer > .menu .btn{
display:inline-block;
background: var(--xrf-primary);
border-radius: 25px;
@ -1136,7 +1232,8 @@ document.head.innerHTML += `
text-align:right;
}
.badge {
.badge,
#messages .msg.ui div.badge{
display:inline-block;
color: var(--xrf-white);
font-weight: bold;
@ -1205,6 +1302,22 @@ document.head.innerHTML += `
top: 64px;
}
.right { float:right }
.left { float:left }
/*
* tabs
*/
div.tab-frame > input{ display:none;}
div.tab-frame > label{ display:block; float:left;padding:5px 10px; cursor:pointer; }
div.tab-frame > input:checked + label{ cursor:default; border-bottom:1px solid #888; font-weight:bold; }
div.tab-frame > div.tab{ display:none; padding:15px 10px 5px 10px;clear:left}
div.tab-frame > input:nth-of-type(1):checked ~ .tab:nth-of-type(1),
div.tab-frame > input:nth-of-type(2):checked ~ .tab:nth-of-type(2),
div.tab-frame > input:nth-of-type(3):checked ~ .tab:nth-of-type(3){ display:block;}
/*
* css icons from https://css.gg
*/
@ -1366,7 +1479,7 @@ document.head.innerHTML += `
position: relative;
display: inline-block;
-moz-transform: rotate(-45deg) scale(var(--ggs,1));
transform: translate(4px,-5px) rotate(-45deg) scale(var(--ggs,1));
transform: translate(4px,1px) rotate(-45deg) scale(var(--ggs,1));
width: 8px;
height: 2px;
background: currentColor;
@ -1532,14 +1645,14 @@ document.head.innerHTML += `
box-sizing: border-box;
position: relative;
display: inline-block;
transform: scale(var(--ggs,1)) translate(3px,2px);
transform: scale(var(--ggs,1)) translate(3px,9px);
width: 16px;
height: 6px;
border: 2px solid;
border-top: 0;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
margin-top: 8px
line-height:15px;
}
.gg-software-download::after {
content: "";
@ -1643,7 +1756,9 @@ window.frontend = (opts) => new Proxy({
.setupIframeUrlHandler()
.setupCapture()
.setupUserHints()
.setupNetworkListeners()
.hidetopbarWhenMenuCollapse()
.hideUIWhenNavigating()
window.notify = this.notify
setTimeout( () => {
@ -1694,12 +1809,66 @@ window.frontend = (opts) => new Proxy({
return this
},
setupNetworkListeners(){
document.addEventListener('network.connect', (e) => {
console.log("network.connect")
window.notify("🪐 connecting to awesomeness..")
$chat.send({message:`🪐 connecting to awesomeness..`,class:['info'], timeout:5000})
})
document.addEventListener('network.connected', (e) => {
window.notify("🪐 connected to awesomeness..")
$chat.visibleChatbar = true
$chat.send({message:`🎉 ${e.detail.plugin.profile.name||''} connected!`,class:['info'], timeout:5000})
})
document.addEventListener('network.disconnect', () => {
window.notify("🪐 disconnecting..")
})
document.addEventListener('network.info', (e) => {
window.notify(e.detail.message)
$chat.send({...e.detail, class:['info'], timeout:5000})
})
document.addEventListener('network.error', (e) => {
window.notify(e.detail.message)
$chat.send({...e.detail, class:['info'], timeout:5000})
})
return this
},
hidetopbarWhenMenuCollapse(){
// hide topbar when menu collapse button is pressed
document.addEventListener('$menu:collapse', (e) => this.el.querySelector("#topbar").style.display = e.detail === true ? 'block' : 'none')
return this
},
hideUIWhenNavigating(){
// hide ui when user is navigating the scene using mouse/touch
let showUI = (show) => (e) => {
let isChatMsg = e.target.closest('.msg')
let isChatLine = e.target.id == 'chatline'
let isChatEmptySpace = e.target.id == 'messages'
let isUI = e.target.closest('.ui')
//console.dir({class: e.target.className, id: e.target.id, isChatMsg,isChatLine,isChatEmptySpace,isUI, tagName: e.target.tagName})
if( isUI || e.target.tagName.match(/^(BUTTON|TEXTAREA|INPUT|A)/) || e.target.className.match(/(btn)/) ) return
if( show ){
$chat.visible = true
}else{
$chat.visible = false
$menu.toggle(false)
}
return true
}
document.addEventListener('mousedown', showUI(false) )
document.addEventListener('mouseup', showUI(true) )
document.addEventListener('touchstart', showUI(false) )
document.addEventListener('touchend', showUI(true) )
},
loadFile(contentLoaders, multiple){
return () => {
window.notify("if you're on Meta browser, file-uploads might be disabled")
@ -1787,24 +1956,28 @@ window.frontend = (opts) => new Proxy({
},
share(opts){
opts = opts || {notify:true,qr:true,share:true}
if( network.connected && !document.location.hash.match(/meet=/) ){
let p = $connections.chatnetwork.find( (p) => p.plugin.name == $connections.selectedChatnetwork )
if( p.link ) document.location.hash += `&meet=${p.link}`
opts = opts || {notify:true,qr:true,share:true,linkonly:false}
if( network.meetingLink && !document.location.hash.match(/meet=/) ){
document.location.hash += `&meet=${network.meetingLink}`
}
if( !document.location.hash.match(/pos=/) ){
document.location.hash += `&pos=${ network.posName || network.pos }`
}
let url = window.location.href
if( opts.linkonly ) return url
this.copyToClipboard( url )
// End of *TODO*
if( opts.notify ){
window.notify(`<h2>${ network.connected ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br>
window.notify(`<h2>${ network.connected ? 'Meeting link ' : 'Link'} copied to clipboard!</h2>
Now share it with your friends <br>
<canvas id="qrcode" width="121" height="121"></canvas><br>
<button onclick="frontend.download()"><i class="gg-software-download"></i>&nbsp;&nbsp;&nbsp;download scene file</button> <br>
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')"><i class="gg-image"></i>&nbsp;&nbsp;download 360 screenshot</button> <br>
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"><i class="gg-serverless"></i>&nbsp;&nbsp;&nbsp;clone & selfhost this experience</a><br>
<br>
To embed this experience in your blog,<br>
copy/paste the following into your HTML:<br><input type="text" value="&lt;iframe src='${document.location.href}'&gt;&lt;/iframe&gt;" id="share"/>
<br>
<br>
`,{timeout:false})
}
// draw QR code
@ -1846,6 +2019,7 @@ window.network = (opts) => new Proxy({
connected: false,
pos: '',
posName: '',
meetinglink: "",
peers: {},
plugin: {},
@ -1901,8 +2075,7 @@ window.network = (opts) => new Proxy({
for ( var i in String.prototype ) add(i)
var a = names[Math.floor(Math.random() * names.length)];
var b = names[Math.floor(Math.random() * names.length)];
var c = names[Math.floor(Math.random() * names.length)];
return String(`${a}-${b}-${c}`).toLowerCase()
return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
}
},
@ -2097,7 +2270,7 @@ document.head.innerHTML += `
height: auto;
margin: 5px 0;
transition: all ease .5s;
border-radius: 3px;
border-radius: 15px;
box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
right: 20px;
position: fixed;
@ -2157,8 +2330,8 @@ document.head.innerHTML += `
.js-snackbar__close {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
align-items: top;
padding: 8px 13px 0px 0px;
user-select: none;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
/*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024
* v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0
*/
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({});
var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
var $el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
@ -627,15 +627,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* 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 )
}
@ -651,9 +653,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)
}
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file})
}
@ -945,6 +935,8 @@ xrf.reset = () => {
// remove mixers
xrf.mixers.map( (m) => m.stop())
xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
}
xrf.parseUrl = (url) => {
@ -979,47 +971,81 @@ 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 || (!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( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
if( !loader ){
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
loader = loader || new Loader().setPath( dir )
}
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)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// 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) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,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.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
})
}
@ -1027,13 +1053,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})
@ -1046,10 +1076,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) => {
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`)
// console.log(` └ applied image '${frag.env.string}' as environment map`)
}
})
@ -1113,20 +1169,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)
//if( !file.match(/\./) || file.match(/\.html/) ){
// debugger
// 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')
//}
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 )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// 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!
//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 )
}
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
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()
}
xrf.frag.rot = function(v, opts){
@ -1240,7 +1300,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
@ -1820,16 +1881,14 @@ 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 static XR fragments
}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 )
})
xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts
let objname = v.fragment
@ -1864,6 +1923,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 = []
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null,
transparent: url.match(/(png|gif)/) ? true : false,
transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide,
color: 0xFFFFFF,
opacity:1
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
}
}
mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true
}

View File

@ -1,5 +1,5 @@
/*
* v0.5.1 generated at Fri Jan 5 11:36:46 AM UTC 2024
* v0.5.1 generated at Mon Jan 29 08:11:09 PM UTC 2024
* https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0
*/
@ -552,10 +552,10 @@ xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({});
var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
var $el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
@ -627,15 +627,17 @@ for ( let i in xrfragment ) xrf[i] = xrfragment[i]
* 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 )
}
@ -651,9 +653,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)
}
@ -905,15 +904,6 @@ xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,model) )
}
// spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view)
xrf.frag.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.emit('parseModel',{model,url,file})
}
@ -945,6 +935,8 @@ xrf.reset = () => {
// remove mixers
xrf.mixers.map( (m) => m.stop())
xrf.mixers = []
// set the player to position 0,0,0
xrf.camera.position.set(0,0,0)
}
xrf.parseUrl = (url) => {
@ -979,47 +971,81 @@ 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 || (!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( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
if( !loader ){
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
loader = loader || new Loader().setPath( dir )
}
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)
}
// clear xrf objects from scene
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
xrf.reset()
// 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) => {
model.file = file
// only change url when loading *another* file
if( xrf.model ) xrf.navigator.pushState( `${dir}${file}`, hash )
xrf.model = model
if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) )
// spec: 2. init metadata inside model for non-SRC data
if( !model.isSRC ){
model.scene.traverse( (mesh) => xrf.hashbus.pub.mesh(mesh,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.defaultPredefinedViews({model,scene:model.scene})
// spec: predefined view(s) & objects-of-interest-in-XRWG from URL (https://xrfragment.org/#predefined_view)
let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments
xrf.add( model.scene )
if( hash ) xrf.navigator.updateHash(hash)
xrf.emit('navigateLoaded',{url,model})
resolve(model)
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})
})
})
}
@ -1027,13 +1053,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})
@ -1046,10 +1076,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) => {
@ -1068,7 +1124,7 @@ xrf.addEventListener('env', (opts) => {
//scene.texture = env.material.map
// renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMappingExposure = 2;
console.log(` └ applied image '${frag.env.string}' as environment map`)
// console.log(` └ applied image '${frag.env.string}' as environment map`)
}
})
@ -1113,20 +1169,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)
//if( !file.match(/\./) || file.match(/\.html/) ){
// debugger
// 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')
//}
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 )
// *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// 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!
//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 )
}
@ -1183,20 +1239,24 @@ xrf.frag.href = function(v, opts){
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()
}
xrf.frag.rot = function(v, opts){
@ -1240,7 +1300,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
@ -1820,16 +1881,14 @@ 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 static XR fragments
}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 )
})
xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts
let objname = v.fragment
@ -1864,6 +1923,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 = []
@ -2055,7 +2115,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null,
transparent: url.match(/(png|gif)/) ? true : false,
transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide,
color: 0xFFFFFF,
opacity:1
@ -2076,6 +2136,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
}
}
mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true
}

View File

@ -64,6 +64,10 @@
`,{timeout:false})
})
</script>
<!-- everything below is completely optional and not part of the spec -->
<script src="./../../../dist/aframe-blink-controls.min.js"></script> <!-- teleporting using controllers -->
<script src="https://cdn.jsdelivr.net/npm/handy-work@3.1.9/build/handy-controls.min.js"></script> <!-- hand controllers -->
<script src="./../../../dist/xrfragment.plugin.p2p.js"></script> <!-- serverless p2p connectivity -->

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
../../assets/query.gltf

View File

@ -0,0 +1 @@
../../assets/index.glb

View File

@ -1 +0,0 @@
assets/index.gltf

View File

@ -4,24 +4,14 @@
<title>THREE.js - xrfragment sandbox</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/css/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
<script src="./../../assets/js/qr.js"></script>
</head>
<body>
<div id="overlay" x-data="{ urls: ['#pos=0,1.6,15','#pos=0,1.6,15&rot=0,360,0'] }">
<img src="./../../assets/logo.png" class="logo"/>
<input type="submit" value="load 3D file"></input>
<input type="text" id="uri" value="" onchange="XRF.navigator.to( $('#uri').value )" style="display:none"/>
<body style="margin:0">
<div id="overlay" style="text-align:right;position:absolute;width:100%;height:100%;top:0;left:0;bottom:0;right:0">
<b>NOTE:</b> this is a THREE barebones example (AFRAME viewer has more features)<br><Br>
<input type="submit" value="load 3D asset"/>
</div>
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"> clone project</a>
<a class="btn-foot" id="embed" target="_blank" onclick="embed()">🔗 share</a>
<a class="btn-foot" id="model" target="_blank" href="index.glb">⬇️ scene</a>
<a class="btn-foot" id="more" target="_blank" >XRF</a>
<textarea style="display:none"></textarea>
<canvas id="qrcode" style="display:none" width="300" height="300"></canvas>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
@ -39,7 +29,6 @@
import xrf from "./../../../dist/xrfragment.three.module.js";
import { loadFile, setupConsole, setupUrlBar, notify } from "./../../assets/js/utils.js";
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';
@ -66,7 +55,6 @@
};
init();
notify("NOTE: only AFRAME demo has immersive back-button (for now)")
function init() {
@ -174,10 +162,6 @@
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
cameraRig.add( controllerGrip2 );
setupConsole()
setupUrlBar( $('input#uri'), XRF )
// Add stats.js
stats = new Stats();
stats.dom.style.width = '80px';
@ -198,11 +182,33 @@
XRF.interactive.add( statsMesh );
})
// util func to bind uploaded files to a loader
let loadFile = function(contentLoaders, multiple){
return function(){
alert("if you're on Meta browser, file-uploads might be disabled")
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = function(){
let files = Array.from(input.files);
let file = files.slice ? files[0] : files
for( var i in contentLoaders ){
let r = new RegExp('\\'+i+'$')
if( file.name.match(r) ) return contentLoaders[i](file)
}
alert(file.name+" is not supported")
};
input.click();
}
}
let fileLoaders = loadFile({
".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null,xrf.loaders.gltf,data) ),
".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null,xrf.loaders.gltf,data) )
".gltf": (file) => file.arrayBuffer().then( (data) => XRF.navigator.to(file.name,null,XRF.loaders.gltf,data) ),
".glb": (file) => file.arrayBuffer().then( (data) => XRF.navigator.to(file.name,null, XRF.loaders.gltf,data) )
})
$("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
$("input[type=submit]").addEventListener("click", fileLoaders )
}

View File

@ -1,8 +1,8 @@
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
var $el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]

View File

@ -72,6 +72,7 @@ xrf.navigator.to = (url,flags,loader,data) => {
}
if( data ){ // file upload
console.dir(loader)
loader.parse(data, "", onLoad )
}else loader.load(url, onLoad )
})

View File

@ -5,10 +5,8 @@ xrf.frag.defaultPredefinedViews = (opts) => {
let frag = xrf.URI.parse( n.userData['#'] )
if( n.parent && n.parent.parent.isScene && document.location.hash.length < 2 ){
xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments
console.log("to")
}else{
xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments
console.log("pub")
}
}
})

View File

@ -10,7 +10,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
mesh.material = new xrf.THREE.MeshBasicMaterial({
map: null,
transparent: url.match(/(png|gif)/) ? true : false,
transparent: url.match(/\.(png|gif)/) ? true : false,
side: THREE.DoubleSide,
color: 0xFFFFFF,
opacity:1
@ -31,6 +31,7 @@ xrf.frag.src.type['image/png'] = function(url,opts){
}
}
mesh.material.map = texture
mesh.material.needsUpdate = true
mesh.needsUpdate = true
}